Compare commits

..

No commits in common. "develop" and "2.17.1-RC5" have entirely different histories.

2103 changed files with 22808 additions and 77165 deletions

View File

@ -88,9 +88,6 @@
:dynamic-var-not-earmuffed
{:level :off}
:type-mismatch
{:level :off}
:used-underscored-binding
{:level :warning}

View File

@ -1,41 +0,0 @@
def specs: [.. | objects | select(has("tests") and has("file"))];
def dur: [.tests[].results[]?.duration // 0] | add;
specs as $s
| ($s | map(select(any(.tests[]; .status == "unexpected")))) as $failed
| ($s | map(select(any(.tests[]; .status == "flaky")))) as $flaky
| ($s | map(select(any(.tests[]; .status == "skipped")))) as $skipped
| ($s | length) as $total
| ($s | map(dur) | add // 0 | . / 1000 | floor) as $cpu
| (if ($failed | length) > 0 then "❌"
elif ($flaky | length) > 0 then "⚠️"
else "✅" end) as $icon
| "## \($icon) Integration tests\n\n"
+ "| Total | Passed | Flaky | Failed | Skipped | Test time |\n"
+ "|---|---|---|---|---|---|\n"
+ "| \($total) | \($total - ($failed|length) - ($flaky|length) - ($skipped|length)) "
+ "| \($flaky|length) | \($failed|length) | \($skipped|length) | \($cpu / 60 | floor)m |\n"
+ (if ($failed | length) > 0 then
"\n### Failed\n\n"
+ ($failed | map("- `\(.file):\(.line)` — \(.title)") | join("\n")) + "\n"
else "" end)
+ (if ($flaky | length) > 0 then
"\n### Flaky (passed on retry)\n\n"
+ ($flaky
| map({ t: "`\(.file):\(.line)` — \(.title)",
r: ([.tests[].results[]? | select(.status == "failed")] | length) })
| sort_by(-.r)
| map("- \(.t) _(\(.r) \(if .r == 1 then "retry" else "retries" end))_")
| join("\n")) + "\n"
else "" end)
+ (if $total > 0 then
"\n<details><summary>Slowest specs</summary>\n\n"
+ ($s | map({ t: "`\(.file)` — \(.title)", d: (dur / 1000 | floor) })
| sort_by(-.d) | .[0:5]
| map("- \(.t) — \(.d)s") | join("\n"))
+ "\n\n</details>\n"
else "" end)

View File

@ -3,7 +3,7 @@ name: Auto Label and Add to Project
on:
issues:
types: [opened]
pull_request_target:
pull_request:
types: [opened]
jobs:

View File

@ -9,6 +9,16 @@ on:
type: string
required: true
default: 'develop'
build_wasm:
description: 'BUILD_WASM. Valid values: yes, no'
type: string
required: false
default: 'yes'
build_storybook:
description: 'BUILD_STORYBOOK. Valid values: yes, no'
type: string
required: false
default: 'yes'
workflow_call:
inputs:
gh_ref:
@ -16,21 +26,29 @@ on:
type: string
required: true
default: 'develop'
build_wasm:
description: 'BUILD_WASM. Valid values: yes, no'
type: string
required: false
default: 'yes'
build_storybook:
description: 'BUILD_STORYBOOK. Valid values: yes, no'
type: string
required: false
default: 'yes'
concurrency:
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
cancel-in-progress: true
jobs:
# ── 1. Decide whether there is anything to build ───────────────────────
check:
name: Check current bundle
runs-on: penpot-standar-runner
timeout-minutes: 10
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
exists: ${{ steps.check.outputs.exists }}
build-bundle:
name: Build and Upload Penpot Bundle
runs-on: penpot-runner-01
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
steps:
- name: Checkout repository
@ -45,52 +63,10 @@ jobs:
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
# The uploaded zip carries its version as S3 metadata. If the
# existing object was already built from this same commit, the
# whole build job is skipped.
- name: Check if this bundle is already built
id: check
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
EXISTING_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "penpot-${{ steps.vars.outputs.gh_ref }}.zip" \
--query 'Metadata."bundle-version"' \
--output text 2>/dev/null || echo "none")
if [ "$EXISTING_VERSION" = "${{ steps.vars.outputs.bundle_version }}" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
{
echo "### ⏭️ Bundle build skipped"
echo ""
echo "The bundle in S3 was already built from \`${{ steps.vars.outputs.bundle_version }}\`."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
# ── 2. Build and upload, only when needed ──────────────────────────────
build:
name: Build and Upload Penpot Bundle
runs-on: penpot-standar-runner
timeout-minutes: 90
needs: check
if: needs.check.outputs.exists == 'false'
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Build bundle
env:
BUILD_WASM: 'yes'
BUILD_STORYBOOK: 'yes'
BUILD_WASM: ${{ inputs.build_wasm }}
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
run: ./manage.sh build-bundle
- name: Prepare directories for zipping
@ -104,32 +80,18 @@ jobs:
zip -r zips/penpot.zip penpot
- name: Upload Penpot bundle to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
aws s3 cp zips/penpot.zip \
s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.check.outputs.gh_ref }}.zip \
--metadata bundle-version=${{ needs.check.outputs.bundle_version }}
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip --metadata bundle-version=${{ steps.vars.outputs.bundle_version }}
# ── 3. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-standar-runner
timeout-minutes: 5
needs: [check, build]
if: failure()
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 📦 *[PENPOT] Error building penpot bundles.*
📄 Triggered from ref: `${{ needs.check.outputs.gh_ref || inputs.gh_ref }}`
Bundle version: `${{ needs.check.outputs.bundle_version || 'n/a' }}`
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
Bundle version: `${{ steps.vars.outputs.bundle_version }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@ -11,6 +11,8 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@ -18,9 +20,3 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "develop"

View File

@ -1,91 +0,0 @@
name: Admin Console Docker Builder
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref to build in penpot-nitrate'
type: string
required: true
default: 'develop'
dispatch_ref:
description: 'Branch of penpot-nitrate from which the workflow definition is read'
type: string
required: false
default: 'develop'
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch or ref to build in penpot-nitrate'
type: string
required: true
dispatch_ref:
description: 'Branch of penpot-nitrate from which the workflow definition is read'
type: string
required: false
default: 'develop'
secrets:
ORG_WORKFLOW_TOKEN:
description: 'Token with Actions write access on penpot-nitrate'
required: true
jobs:
build-nitrate-docker:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.ORG_WORKFLOW_TOKEN }}
REPO: penpot/penpot-nitrate
WORKFLOW: build-docker-admin-console.yml
GH_REF: ${{ inputs.gh_ref }}
DISPATCH_REF: ${{ inputs.dispatch_ref }}
steps:
- name: Trigger nitrate docker build
id: dispatch
run: |
DISTINCT_ID="${{ github.run_id }}-${{ github.run_attempt }}"
CALLER_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$DISPATCH_REF" \
-f gh_ref="$GH_REF" \
-f caller_run_id="$DISTINCT_ID" \
-f caller_run_url="$CALLER_URL"
# Locate the dispatched run using the correlation id embedded in its run-name
RUN_ID=""
for i in $(seq 1 24); do
sleep 5
RUN_ID=$(gh run list --repo "$REPO" --workflow "$WORKFLOW" \
--limit 10 --json databaseId,displayTitle \
--jq ".[] | select(.displayTitle | contains(\"$DISTINCT_ID\")) | .databaseId" \
| head -n1)
[ -n "$RUN_ID" ] && break
done
if [ -z "$RUN_ID" ]; then
echo "::error::Could not locate the dispatched run in $REPO"
exit 1
fi
RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID"
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
echo "run_url=$RUN_URL" >> "$GITHUB_OUTPUT"
echo "::notice title=Nitrate docker build::$RUN_URL"
- name: Wait for nitrate docker build
run: |
gh run watch "${{ steps.dispatch.outputs.run_id }}" \
--repo "$REPO" \
--interval 30 \
--exit-status
- name: Report result
if: always() && steps.dispatch.outputs.run_id != ''
run: |
CONCLUSION=$(gh run view "${{ steps.dispatch.outputs.run_id }}" \
--repo "$REPO" --json conclusion --jq '.conclusion')
{
echo "### 🐳 Nitrate docker build"
echo ""
echo "- Result: \`${CONCLUSION:-in_progress}\`"
echo "- Run: ${{ steps.dispatch.outputs.run_url }}"
} >> "$GITHUB_STEP_SUMMARY"

View File

@ -6,7 +6,7 @@ on:
jobs:
build-and-push:
name: Build and push DevEnv Docker image
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
steps:
- name: Set common environment variables
@ -20,19 +20,12 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry (push destination)
- name: Login to Docker Registry
uses: docker/login-action@v4
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Login to Docker Hardened Images registry (base image pull)
uses: docker/login-action@v4
with:
registry: dhi.io
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Build and push DevEnv Docker image
uses: docker/build-push-action@v7
env:
@ -42,14 +35,12 @@ jobs:
file: ./docker/devenv/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
provenance: mode=max
sbom: true
tags: ${{ env.DOCKER_IMAGE }}:latest
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd

View File

@ -20,117 +20,55 @@ concurrency:
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
cancel-in-progress: true
env:
ALL_IMAGES: backend frontend exporter storybook mcp
# All runner instances live on the same server, so the bundle is
# downloaded from S3 once and shared between build jobs through this
# host-local directory. Each build job falls back to S3 if the file is
# missing (e.g. if runners ever move to separate machines).
BUNDLE_CACHE: /var/tmp/penpot-bundle-cache
jobs:
# ── 1. Resolve the build key and check the whole set at once ───────────
prepare:
name: Prepare
runs-on: penpot-extended-runner
timeout-minutes: 15
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
build_key: ${{ steps.vars.outputs.build_key }}
exists: ${{ steps.check.outputs.exists }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
- name: Extract some useful variables
id: vars
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
GH_REF="${{ inputs.gh_ref || github.ref_name }}"
echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT
BUNDLE_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "penpot-$GH_REF.zip" \
--query 'Metadata."bundle-version"' \
--output text)
echo "bundle_version=$BUNDLE_VERSION" >> $GITHUB_OUTPUT
# Image content = bundle + docker build context, so the build key
# combines both.
CTX_HASH=$(git rev-parse "HEAD:docker/images" | cut -c1-12)
echo "build_key=${BUNDLE_VERSION}-${CTX_HASH}" >> $GITHUB_OUTPUT
# The image set is a single block, so a single set-level check is
# enough: `promote` drops a marker object in S3 only after every
# image was built AND every branch tag was moved. Marker present
# means there is nothing at all to do for this build key.
- name: Check if this image set is already built
id: check
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
if aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "markers/images-${{ steps.vars.outputs.build_key }}" \
> /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
{
echo "### ⏭️ Image set build skipped"
echo ""
echo "The whole set was already built and promoted for \`${{ steps.vars.outputs.build_key }}\`."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "exists=false" >> $GITHUB_OUTPUT
# Stage the bundle in the host-local cache, once, for all the
# build jobs. Download to a temp name and mv for atomicity;
# prune stale bundles while at it.
mkdir -p "$BUNDLE_CACHE"
find "$BUNDLE_CACHE" -type f -mtime +1 -delete || true
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.build_key }}.zip"
if [ ! -f "$ZIP" ]; then
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
fi
fi
# ── 2. One build per image, in parallel, only when needed ──────────────
build:
name: Build ${{ matrix.image }}
runs-on: penpot-extended-runner
timeout-minutes: 60
needs: prepare
if: needs.prepare.outputs.exists == 'false'
strategy:
fail-fast: true
# 4 runner slots are available for build jobs on this server; cap the
# matrix at 3 so short jobs (prepare and other workflows' checks)
# never queue behind long builds.
max-parallel: 3
matrix:
image: [backend, frontend, exporter, storybook, mcp]
build-and-push:
name: Build and Push Penpot Docker Images
runs-on: penpot-runner-02
steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}-${{ matrix.image }}" >> $GITHUB_ENV
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Download Penpot Bundles
id: bundles
env:
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
tmp=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "$FILE_NAME" \
--query 'Metadata."bundle-version"' \
--output text)
echo "bundle_version=$tmp" >> $GITHUB_OUTPUT
pushd docker/images
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
unzip $FILE_NAME > /dev/null
mv penpot/backend bundle-backend
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
mv penpot/storybook bundle-storybook
mv penpot/mcp bundle-mcp
popd
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
uses: docker/login-action@v4
with:
@ -147,140 +85,103 @@ jobs:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# Images now build FROM Docker Hardened Images (dhi.io). DHI
# is free (Apache 2.0, no subscription), but pulling from it
# still requires an authenticated login -- a separate `docker
# login` against a different registry host, even though it
# reuses the same PUB_DOCKER_* credentials as the DockerHub
# login above.
- name: Login to Docker Hardened Images registry (base image pull)
uses: docker/login-action@v4
with:
registry: dhi.io
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# Bundle staged once by `prepare` on this host; the S3 fallback only
# triggers if the cache is unavailable (runners on another machine,
# cache pruned mid-run, ...).
- name: Prepare Penpot bundle
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
ZIP="$BUNDLE_CACHE/penpot-${{ needs.prepare.outputs.build_key }}.zip"
if [ ! -f "$ZIP" ]; then
echo "Bundle not found in host cache; falling back to S3."
mkdir -p "$BUNDLE_CACHE"
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.prepare.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
fi
# Extract only the bundle this job needs.
pushd docker/images
unzip -q "$ZIP" "penpot/${{ matrix.image }}/*"
mv "penpot/${{ matrix.image }}" "bundle-${{ matrix.image }}"
popd
- name: Set up QEMU (stable)
uses: docker/setup-qemu-action@v4
with:
platforms: linux/amd64,linux/arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ matrix.image }}
images:
frontend
backend
exporter
storybook
mcp
labels: |
bundle_version=${{ needs.prepare.outputs.bundle_version }}
bundle_version=${{ steps.bundles.outputs.bundle_version }}
- name: Build and push Docker image
- name: Build and push Backend Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'backend'
BUNDLE_PATH: './bundle-backend'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.${{ matrix.image }}
file: ./docker/images/Dockerfile.backend
platforms: linux/amd64,linux/arm64
push: true
provenance: mode=max
sbom: true
# Immutable tag only; branch tags are moved atomically for the
# whole image set by the `promote` job.
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:build-${{ needs.prepare.outputs.build_key }}
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache,mode=max
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
# ── 3. Move the branch tags of ALL images together ─────────────────────
# Runs only when every build succeeded (default `needs` semantics); if
# the set was already complete, `build` is skipped and so is this job —
# the S3 marker guarantees the branch tags were already moved.
promote:
name: Promote image set
runs-on: penpot-extended-runner
timeout-minutes: 10
needs: [prepare, build]
steps:
- name: Set common environment variables
run: |
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
uses: docker/login-action@v4
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Point branch tags to the new build key
run: |
set -e
for image in $ALL_IMAGES; do
docker buildx imagetools create \
-t "${{ secrets.DOCKER_REGISTRY }}/$image:${{ needs.prepare.outputs.gh_ref }}" \
"${{ secrets.DOCKER_REGISTRY }}/$image:build-${{ needs.prepare.outputs.build_key }}"
done
# The marker is written LAST: its presence certifies that all five
# images exist and all branch tags point to this build key.
- name: Write set-completed marker
- name: Build and push Frontend Docker image
uses: docker/build-push-action@v7
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
echo "${{ github.run_id }}" | aws s3 cp - \
"s3://${{ secrets.S3_BUCKET }}/markers/images-${{ needs.prepare.outputs.build_key }}"
{
echo "### ✅ Image set promoted"
echo ""
echo "All \`:${{ needs.prepare.outputs.gh_ref }}\` tags now point to \`build-${{ needs.prepare.outputs.build_key }}\`."
} >> "$GITHUB_STEP_SUMMARY"
DOCKER_IMAGE: 'frontend'
BUNDLE_PATH: './bundle-frontend'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.frontend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
# ── 4. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-extended-runner
timeout-minutes: 5
needs: [prepare, build, promote]
if: failure()
- name: Build and push Exporter Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'exporter'
BUNDLE_PATH: './bundle-exporter'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.exporter
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Storybook Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'storybook'
BUNDLE_PATH: './bundle-storybook'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.storybook
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push MCP Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'mcp'
BUNDLE_PATH: './bundle-mcp'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🐳 *[PENPOT] Error building/promoting the penpot docker image set.*
📄 Triggered from ref: `${{ needs.prepare.outputs.gh_ref || inputs.gh_ref }}`
📦 Bundle: `${{ needs.prepare.outputs.bundle_version || 'n/a' }}`
❌ 🐳 *[PENPOT] Error building penpot docker images.*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
📦 Bundle: `${{ steps.bundles.outputs.bundle_version }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@ -11,6 +11,8 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@ -18,9 +20,3 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "staging"

View File

@ -12,6 +12,8 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@ -20,21 +22,14 @@ jobs:
with:
gh_ref: ${{ github.ref_name }}
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
notify:
name: Notifications
runs-on: ubuntu-24.04
needs:
- build-docker
- build-docker-admin-console
needs: build-docker
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
@ -45,9 +40,7 @@ jobs:
publish-final-tag:
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
needs:
- build-docker
- build-docker-admin-console
needs: build-docker
uses: ./.github/workflows/release.yml
secrets: inherit
with:

View File

@ -1,20 +0,0 @@
name: _TMP TOKENS
on:
workflow_dispatch:
schedule:
- cron: '46 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "hiru-tokens-in-libs"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: "hiru-tokens-in-libs"

View File

@ -131,7 +131,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd

View File

@ -34,7 +34,7 @@ permissions:
jobs:
deploy:
runs-on: penpot-standar-runner
runs-on: penpot-runner-01
steps:
- name: Checkout
uses: actions/checkout@v6
@ -114,7 +114,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd

View File

@ -129,7 +129,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd

View File

@ -103,7 +103,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd

View File

@ -32,7 +32,7 @@ jobs:
test-backend:
if: ${{ !github.event.pull_request.draft }}
name: "Backend Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:

View File

@ -30,7 +30,7 @@ jobs:
test-common:
if: ${{ !github.event.pull_request.draft }}
name: "Common Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:

View File

@ -1,69 +0,0 @@
name: "CI: Composable Test Suite"
# Runs the composable component test suite (it exercises component semantics
# through the real Plugin API against the full frontend, so it needs the
# frontend bundle + the plugin runtime, but no backend): the driver serves the
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
# fixtures. See plugins/apps/composable-test-suite/README.md ("Running in CI").
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
composable-test-suite:
if: ${{ !github.event.pull_request.draft }}
name: "Run composable test suite (mocked backend)"
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# The driver serves the prebuilt bundle from frontend/resources/public.
- name: Build frontend bundle
working-directory: ./frontend
run: ./scripts/build
- name: Install deps
working-directory: ./plugins
run: |
corepack enable;
corepack install;
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter composable-test-suite exec playwright install --with-deps chromium
- name: Run composable test suite (mocked)
working-directory: ./plugins
run: pnpm --filter composable-test-suite run test:ci

View File

@ -1,58 +0,0 @@
name: "CI: Exporter"
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'exporter/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'exporter/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-exporter:
if: ${{ !github.event.pull_request.draft }}
name: "Exporter Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Lint
working-directory: ./exporter
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:clj
pnpm run lint:clj
- name: Tests
working-directory: ./exporter
run: |
./scripts/test

View File

@ -34,7 +34,7 @@ jobs:
test-frontend:
if: ${{ !github.event.pull_request.draft }}
name: "Frontend Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:

View File

@ -5,37 +5,11 @@ defaults:
shell: bash
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
shards:
description: 'Shard layout (JSON array)'
type: choice
required: true
default: '[1, 2, 3, 4]'
options:
- '[1, 2, 3, 4]'
- '[1, 2, 3, 4, 5, 6]'
- '[1, 2]'
- '[1]'
workers:
description: 'Playwright workers per shard'
type: string
required: true
default: '2'
pull_request:
paths:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
- '.github/workflows/tests-integration.yml'
types:
- opened
@ -51,41 +25,25 @@ on:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
- '.github/workflows/tests-integration.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Build Integration Bundle"
runs-on: penpot-extended-runner
timeout-minutes: 30
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
outputs:
bundle_key: ${{ steps.vars.outputs.bundle_key }}
steps:
# An empty `ref` makes checkout fall back to its default (the PR merge
# ref on pull_request, the pushed ref on push).
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
# The cache key must come from the SHA actually checked out: on a manual
# run `github.sha` points at the dispatching ref, not at `gh_ref`.
- name: Extract cache key
id: vars
run: |
echo "bundle_key=integration-bundle-$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Build Bundle
working-directory: ./frontend
@ -95,151 +53,41 @@ jobs:
- name: Store Bundle Cache
uses: actions/cache@v5
with:
key: ${{ steps.vars.outputs.bundle_key }}
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests (${{ matrix.shard }})"
runs-on: penpot-extended-runner
timeout-minutes: ${{ github.base_ref == 'staging' && 60 || 25 }}
needs: build-integration
# TEMPORARY (release stabilization): PRs targeting `staging` run on a
# single serial shard, so new flakes cannot block the release work.
# Remove the `github.base_ref` branch below to restore full parallelism.
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(inputs.shards || (github.base_ref == 'staging' && '[1]' || '[1, 2, 3, 4]')) }}
name: "Integration Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
- /var/cache/github-runner/ms-playwright:/ms-playwright
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
- name: Restore Cache
uses: actions/cache/restore@v5
with:
key: ${{ needs.build-integration.outputs.bundle_key }}
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Install deps
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile;
# No-op once the shared volume is warm; keeps the first run working.
- name: Install Playwright Chromium
working-directory: ./frontend
run: pnpm exec playwright install chromium
# `strategy.job-total` is the matrix size, so the shard denominator
# follows the `shards` input without being hardcoded.
- name: Run Tests
working-directory: ./frontend
env:
WORKERS: ${{ inputs.workers }}
BASE_REF: ${{ github.base_ref }}
run: |
# TEMPORARY (release stabilization): see the note on the matrix above.
if [ -z "$WORKERS" ]; then
if [ "$BASE_REF" = "staging" ]; then WORKERS=1; else WORKERS=2; fi
fi
echo "Running shard ${{ matrix.shard }}/${{ strategy.job-total }} with $WORKERS workers"
pnpm exec playwright test --project default \
--workers="$WORKERS" \
--shard=${{ matrix.shard }}/${{ strategy.job-total }} \
--reporter=blob
- name: Upload blob report
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-blob-report-${{ matrix.shard }}
path: frontend/blob-report/
overwrite: true
retention-days: 3
./scripts/test-e2e
- name: Upload test result
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-tests-result-${{ matrix.shard }}
name: integration-tests-result
path: frontend/test-results/
overwrite: true
if-no-files-found: ignore
retention-days: 3
merge-reports:
if: ${{ always() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }}
name: "Merge Integration Reports"
runs-on: penpot-extended-runner
timeout-minutes: 15
needs: test-integration
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
- name: Install deps
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile;
- name: Download blob reports
uses: actions/download-artifact@v7
with:
path: frontend/all-blob-reports
pattern: integration-blob-report-*
merge-multiple: true
- name: Merge into HTML report
working-directory: ./frontend
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: report.json
run: |
pnpm exec playwright merge-reports \
--reporter=html,json,list ./all-blob-reports
- name: Test summary
if: always()
working-directory: ./frontend
run: |
if [ ! -f report.json ]; then
echo "No report produced (all shards failed early)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
jq -r -f ../.github/scripts/playwright-summary.jq report.json >> "$GITHUB_STEP_SUMMARY"
- name: Upload HTML report
uses: actions/upload-artifact@v7
with:
name: integration-html-report
path: frontend/playwright-report/
overwrite: true
retention-days: 7

View File

@ -32,7 +32,7 @@ jobs:
test-library:
if: ${{ !github.event.pull_request.draft }}
name: "Library Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:

View File

@ -1,4 +1,4 @@
name: "CI: MCP"
name: "MCP CI"
on:
pull_request:
@ -28,7 +28,7 @@ jobs:
test-mcp:
if: ${{ !github.event.pull_request.draft }}
name: "Test MCP"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:

View File

@ -53,7 +53,7 @@ jobs:
api-test-suite-mocked:
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
name: "Run Plugin API Test Suite (mocked)"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
@ -95,7 +95,7 @@ jobs:
# api-test-suite-live:
# if: ${{ github.event_name == 'workflow_dispatch' }}
# name: Run Plugin API Test Suite (live)
# runs-on: penpot-extended-runner
# runs-on: penpot-runner-02
# container:
# image: penpotapp/devenv:latest
#

View File

@ -30,7 +30,7 @@ jobs:
test-plugins:
if: ${{ !github.event.pull_request.draft }}
name: Plugins Runtime Linter & Tests
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:

View File

@ -30,7 +30,7 @@ jobs:
test-render-wasm:
if: ${{ !github.event.pull_request.draft }}
name: "Render WASM Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:

3
.gitignore vendored
View File

@ -58,8 +58,6 @@ opencode.json
/docker/images/bundle*
/exporter/target
/exporter/.shadow-cljs
/exporter/resources/wasm/
/exporter/src/app/wasm/shared.js
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/playwright-report/
@ -90,7 +88,6 @@ opencode.json
/blob-report/
/playwright/.cache/
/render-wasm/target/
/media-processor/dist/
/**/node_modules
/**/.yarn/*
/.pnpm-store

2
.nvmrc
View File

@ -1 +1 @@
v24.19.0
v24.18.0

View File

@ -0,0 +1,55 @@
---
name: commiter
description: Git commit assistant
mode: subagent
permission:
read: allow
glob: allow
grep: allow
edit: deny
webfetch: deny
websearch: deny
task: deny
skill: deny
lsp: deny
todowrite: deny
question: deny
external_directory: deny
bash: allow
---
## Role
You are the Penpot commit assistant. You produce git commits that follow the
repository's commit conventions. You do not implement features, review code, or
push branches — you commit.
## Required Reading
Before drafting any commit, **read `.serena/memories/workflow/creating-commits.md`
end-to-end**. It is the authoritative source for the commit message format, the
emoji menu, subject/body limits, and the `AI-assisted-by` trailer. Follow it
exactly — do not improvise the format and do not restate its contents here.
## Pre-commit Workflow
1. **Stage the files** specified by the calling agent. Do not ask for
confirmation — the calling agent knows exactly which files to commit.
2. Run `git diff --staged` to review the content. If you see secrets (API
keys, tokens, passwords, private keys, `.env` values), debug prints, or
anything that does not match the stated intent, STOP and tell the user
before committing.
3. Following the format in the doc, draft the message and run
`git commit -m "<subject>" -m "<body>"` (or `git commit -F -` if the body has
unusual characters). The `AI-assisted-by` trailer value is provided by the
calling agent — use it verbatim.
## Constraints
- Do not push. Pushing is a separate workflow handled by the user.
- Do not run `git reset`, `git checkout`, `git restore`, `git clean`, or `rm` — these are destructive operations.
- Do not pass `--author`. Author identity comes from the local git config.
- Do not amend a commit you did not create in this session, unless the user explicitly asks.
- Do not bypass pre-commit hooks (`--no-verify`) unless the user explicitly asks.
- Do not add untracked files that were not created in this session.
- Do not ask questions. The calling agent provides all necessary information. If something is unclear, proceed with what you know and note any assumptions in your response.

View File

@ -1,5 +1,5 @@
---
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the create-commit skill
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the commiter subagent
agent: build
---
@ -32,11 +32,12 @@ Implement the prepared plan from the session context. Work methodically, keeping
changes focused on what the issue requires. Do not commit — the commit happens in
step 4.
## 4. Commit with the create-commit skill
## 4. Commit with the commiter subagent
After the implementation is complete, load the **`create-commit`** skill and
follow its workflow to commit the changes. Provide a brief summary of what was
implemented and why, the issue reference (`issue-NNNN`), and the model name you
are running as so the `AI-assisted-by` trailer is set correctly.
After the implementation is complete, delegate the commit to the **`commiter`**
subagent. Give it a brief summary of what was implemented and why, the issue
reference (`issue-NNNN`), and the model name you are running as so it sets the
`AI-assisted-by` trailer correctly. The subagent owns the commit format and
conventions.
Do not push. Pushing is handled separately by the user.

View File

@ -1,40 +0,0 @@
---
description: Resolve local git conflicts and stage the resolved files with git add — never continues the rebase
agent: build
---
# Fix Git Conflicts
Resolve conflicts in the local repository. The user handles finishing the
rebase themselves — you must **never** run `git rebase --continue`,
`git rebase --skip`, `git merge --continue`, or anything similar.
## Phase 1 — Understand the problem (read-only)
1. Run `git status` to detect the conflict state (rebase, merge, cherry-pick, etc.) and list conflicted files.
2. For each conflicted (unmerged) file, understand the situation **without modifying anything**:
- Read the file and identify the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`).
- Inspect both sides — `git show <ours>:<file>` and `git show <theirs>:<file>` — plus `git log`/`git show` on the commits involved to understand intent.
- Identify what each side changed and why, and how they should be combined.
## Phase 2 — Present the resolution plan
3. **Present a clear plan to the user before touching any file.** For each conflicted file, state:
- What each side changed and why.
- Your proposed resolution and the reasoning behind it.
- How the two sides are combined (both additive → merge; both modify the same code → keep the semantically correct version, merging intent from both sides when clear from code and context).
4. **Ask the user only when genuinely unclear.** Do not ask about anything you can determine yourself from the code, commit messages, or context. Only decisions that are not determinable and change the outcome (e.g. conflicting product decisions, which side to discard) warrant a question. **Collect all such questions together in an "Open Questions" section at the end of the plan**, so the user has full context to answer them properly.
5. **Wait for the user to accept the plan** (and answer any open questions) before editing, staging, or otherwise modifying anything.
## Phase 3 — Execute
6. Resolve each conflicted file by editing the file to the agreed merged content and removing all conflict markers.
## Phase 4 — Stage and verify
7. **Stage every resolved file** with `git add <file>`. Do not stage unrelated untracked files unless clearly part of the resolution.
8. Verify no conflict markers remain (search for `<<<<<<<` / `>>>>>>>` in resolved files) and that `git status` shows no unmerged paths.
## Phase 5 — Report
9. Briefly report the conflict state, how each conflicted file was resolved (and any answers received to open questions), and stop — do **not** run `git rebase --continue` or any other continuation command.

View File

@ -1,24 +1,16 @@
Act as a senior software engineer and perform a thorough review.
Act as a senior software engineer and perform a thorough code review.
## Instructions
1. **Determine what is being reviewed** from the provided context:
- **If it is a plan** (implementation plan, design document, task breakdown) → load the **`plan-review`** skill.
- **If it is code** (diff, PR, code change) → load the **`code-review`** skill.
1. Load the **`code-review-and-quality`** skill — it defines the five axes, core principles (DRY, KISS, YAGNI), severity taxonomy, and output format.
2. Determine the diff or code to review from the provided context.
3. Read the diff and the surrounding context for each changed file.
4. Review across all five axes: correctness, readability, architecture, security, performance.
5. Produce the review using the **Review Output** format from the skill (Summary → Critical/High → Other Findings → Refactoring → Testing Recommendations → Positive Observations → Final Verdict).
6. For each finding: state the severity (Critical / High / Medium / Low / Suggestion), identify the file and line, describe failure circumstances, and propose a concrete fix.
7. Do not invent problems. Every finding must be real and actionable.
2. Read `AGENTS.md` and follow its instructions for finding and reading all related testing documentation from memories before reviewing.
3. **Skip generated files, lockfile-only changes, and unrelated modifications** unless they introduce security risks.
4. Follow the loaded skill's process and produce its output format.
## Strong Rules
1. Do not invent problems. Every finding must be real and actionable.
2. Do not modify any code and do not create a commit — this command only reviews.
3. Be specific and constructive. "This could be better" is not helpful — explain why and how.
4. Prioritize by impact. One structural issue outweighs ten nits.
5. Missing tests are an issue, not a suggestion. Report as a severity-tagged finding — never as a recommendation.
Do not modify any code and do not create a commit — this command only reviews.
## Context

View File

@ -1,5 +1,5 @@
---
name: code-review
name: code-review-and-quality
description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.
---

View File

@ -1,47 +0,0 @@
---
name: create-commit
description: Stage, review, and commit files following Penpot commit conventions.
---
# Skill: create-commit
Produce a git commit that follows Penpot's commit message conventions. This
skill owns the commit format, staging review, and safety checks — it does not
implement features or push.
## When to Use
- After code changes are complete and files need to be committed
- When delegated by a workflow step (e.g. implement-plan) to handle the commit
## Required Reading
Before drafting any commit, read `mem:workflow/creating-commits` end-to-end. It
is the authoritative source for the commit message format, the emoji menu,
subject/body limits, and the `AI-assisted-by` trailer. Follow it exactly.
## Workflow
1. **Stage the files** specified by the calling context. Do not ask for
confirmation.
2. Run `git diff --staged` to review the content. If you see secrets (API keys,
tokens, passwords, private keys, `.env` values), debug prints, or anything
that does not match the stated intent, **STOP** and tell the user before
committing.
3. Draft the message following the format in the memory doc, wrapping the body
at 72 characters per line, and run:
```bash
git commit -m "<subject>" -m "<body>"
```
(or `git commit -F -` if the body has unusual characters).
4. The `AI-assisted-by` trailer value is provided by the calling context — use
it verbatim.
## Constraints
- Do not push. Pushing is a separate workflow handled by the user.
- Do not run `git reset`, `git checkout`, `git restore`, `git clean`, or `rm`.
- Do not pass `--author`. Author identity comes from the local git config.
- Do not amend a commit you did not create in this session, unless explicitly asked.
- Do not bypass pre-commit hooks (`--no-verify`) unless explicitly asked.
- Do not add untracked files that were not created in this session.

View File

@ -1,315 +0,0 @@
---
name: plan-review
description: Reviews implementation plans for quality, completeness, and actionability. Use after a plan is produced by the planner skill, before starting implementation. Use when evaluating a plan written by yourself, another agent, or a human.
---
# Plan Review
## Overview
Multi-dimensional plan review with quality gates. Every plan gets reviewed before implementation starts — no exceptions. Review covers six axes: completeness, task quality, architecture & sequencing, risk coverage, actionability, and proposed code quality.
**The approval standard:** Approve a plan when it is specific enough that a skilled implementer could execute it without guessing, the task ordering is sound, and risks are acknowledged. Perfect plans don't exist — the goal is confidence that implementation won't derail. Don't block a plan because it isn't exactly how you would have structured it. If it's executable and well-organized, approve it.
## When to Use
- After the planner skill produces a plan
- Before starting implementation on any non-trivial task
- When reviewing a plan written by another agent or a human
- When a plan feels too large, vague, or risky to start
**Do NOT use for:** Single-file changes with obvious scope, or when the task is trivial enough to just do.
## The Six-Axis Review
Every plan gets evaluated across these dimensions:
### 1. Completeness
Does the plan cover everything needed to implement successfully?
- Is the **context** clear? (What problem, why now, what's the goal?)
- Are **affected modules** identified with paths?
- Are **architecture decisions** documented with rationale?
- Is there a **testing strategy**?
- Are **verification commands** explicit (not "run the tests")?
- Are **open questions** listed (not buried in someone's head)?
- Is there a **parallelization** assessment for multi-task plans?
**Missing any of these is a gap, not a nit.**
### 2. Task Quality
Are the tasks well-defined and independently executable?
- Does every task have **acceptance criteria**? (Testable, not vague)
- Does every task have **verification steps**?
- Are tasks **sized appropriately**? (XSM is ideal, L is acceptable, XL must be split)
- Are **dependencies** between tasks explicitly stated?
- Are **files likely touched** listed?
- Is each task a **single, self-contained change**? (Not "implement the whole feature")
- Could a skilled implementer pick up any task and execute it without asking clarifying questions?
### 3. Architecture & Sequencing
Is the plan structured so implementation flows correctly?
- Does implementation order follow the **dependency graph** (foundations first)?
- Are tasks **vertically sliced** (feature paths) rather than horizontally layered?
- Does each task leave the system in a **working state**?
- Are there **checkpoints** between major phases?
- Are **high-risk tasks early** (fail fast)?
- Is the total plan a reasonable number of tasks? (More than ~15 tasks suggests the scope should be split into multiple plans)
### 4. Risk Coverage
Are the hard parts acknowledged and mitigated?
- Are **edge cases** identified?
- Are **breaking changes** or **migration concerns** noted?
- Are **security implications** considered?
- Are **performance implications** considered?
- Are **external dependencies** or integration risks flagged?
- Is there a plan for **rollback** if something goes wrong?
- Are **data integrity** risks addressed (what happens if a migration fails mid-way)?
### 5. Actionability
Can an implementer actually execute this?
- Are **file paths** specific (not "update the relevant files")?
- Are **function/method names** mentioned where applicable?
- Are **verification commands** copy-pasteable (not "run the linter")?
- Are **test commands** project-specific (not generic)?
- Is the **code shape** described where the implementation isn't obvious?
- Are **conventions** referenced (naming, patterns, existing utilities to reuse)?
- Does the plan reference **existing code** the implementer should read first?
### 6. Proposed Code Quality *(when the plan includes implementation details)*
If the plan proposes code shapes, function signatures, data structures, or API designs, evaluate those proposals against `code-review` criteria:
- **Correctness:** Do the proposed types/signatures handle edge cases (null, empty, boundaries)?
- **Readability:** Are proposed names descriptive and consistent with project conventions?
- **Architecture:** Do proposed abstractions follow existing patterns? Are they justified (not over-engineered)?
- **Security:** Do proposed APIs validate input at boundaries? Any injection/XSS vectors in the design?
- **Performance:** Do proposed data structures avoid N+1 patterns? Any unbounded operations in the design?
**When to apply:** Only when the plan includes specific code snippets, type definitions, API contracts, or function signatures. Plans that only describe "what" without showing "how" skip this axis.
## Structural Remedies
When you flag a structural problem in a plan, propose the fix — not just the problem:
- **A task is too large (XL):** Split it into vertical slices. Each slice should be independently testable.
- **Missing acceptance criteria:** Draft 23 specific, testable conditions for the task.
- **Wrong sequencing:** Identify the dependency and propose the correct order.
- **No checkpoints:** Suggest where checkpoints should go (typically after every 23 tasks).
- **Vague verification:** Replace "run tests" with the actual project command.
- **Horizontal slicing:** Restructure into vertical feature paths.
- **Missing risk section:** Draft the risks you can identify from the plan content.
Prefer the remedy that makes the plan immediately actionable over one that just flags the gap.
## Plan Sizing
Plans should be scoped to a single deliverable:
```
15 tasks → Good. A focused feature or bug fix.
610 tasks → Acceptable for a moderate feature.
1115 tasks → Large. Consider splitting into phases.
15+ tasks → Too large. Split into multiple plans.
```
**What counts as "one plan":** A self-contained set of changes that delivers a single coherent capability. If you can describe the goal in one sentence, it's one plan.
## Categorize Findings
Label every comment with its severity so the author knows what's required vs optional:
| Prefix | Meaning | Author Action |
|--------|---------|---------------|
| *(no prefix)* | Required change | Must address before implementation starts |
| **Critical:** | Blocks implementation | Missing security consideration, data integrity risk, fundamentally wrong approach |
| **Nit:** | Minor, optional | Author may ignore — wording, formatting |
| **Optional:** / **Consider:** | Suggestion | Worth considering but not required |
| **FYI** | Informational only | No action needed — context for future reference |
**Lead with what matters.** Order findings by leverage: missing risks and wrong sequencing first, then task quality gaps, then completeness, then nits. If you have one critical sequencing problem and ten nits, the sequencing problem *is* the review.
## Review Process
### Step 1: Understand the Goal
Before evaluating structure, understand intent:
```
- What is this plan trying to accomplish?
- What problem does it solve?
- What does "done" look like?
```
### Step 2: Check Completeness First
Scan for missing sections before diving into content:
```
- Context present?
- Affected modules listed?
- Architecture decisions documented?
- Risks acknowledged?
- Testing strategy defined?
- Verification commands explicit?
```
### Step 3: Review Task Quality
Walk through each task:
```
For each task:
1. Can I tell exactly what to build?
2. Are acceptance criteria specific and testable?
3. Is the size reasonable (not XL)?
4. Are dependencies clear?
5. Would I know which files to touch?
```
### Step 4: Validate Sequencing
Check the dependency graph:
```
- Are foundations built first?
- Does each task leave the system working?
- Are checkpoints placed correctly?
- Are high-risk items early?
- Is it vertically sliced?
```
### Step 5: Assess Actionability
Put yourself in the implementer's shoes:
```
- Could I pick up task 1 and start coding without asking any questions?
- Are the verification commands copy-pasteable?
- Are file paths and function names specific?
- Is existing code referenced where I'd need to read it?
```
### Step 6: Verify the Verification Story
Check that the plan can actually confirm it worked:
```
- What tests should pass after implementation?
- What build/compile commands are relevant?
- What manual checks are needed?
- How do we know the feature works end-to-end?
```
### Step 7: Evaluate Proposed Code Quality *(if applicable)*
If the plan includes code snippets, types, or API designs:
```
- Load code-review skill for criteria
- Check proposed signatures for edge cases
- Verify naming follows project conventions
- Confirm abstractions follow existing patterns
- Scan for security vectors in proposed APIs
- Check for performance issues in proposed data structures
```
## Review Checklist
```markdown
## Review: [Plan title]
### Completeness
- [ ] Context explains the problem and goal
- [ ] Affected modules are listed with paths
- [ ] Architecture decisions have rationale
- [ ] Testing strategy is defined
- [ ] Verification commands are explicit and project-specific
- [ ] Open questions are listed
### Task Quality
- [ ] Every task has acceptance criteria
- [ ] Every task has verification steps
- [ ] Tasks are sized XSM (L acceptable, XL must be split)
- [ ] Task dependencies are stated
- [ ] Files likely touched are listed
### Architecture & Sequencing
- [ ] Order follows dependency graph (foundations first)
- [ ] Vertically sliced (not horizontal layers)
- [ ] Each task leaves system working
- [ ] Checkpoints exist between phases
- [ ] High-risk tasks are early
### Risk Coverage
- [ ] Edge cases identified
- [ ] Breaking changes / migrations noted
- [ ] Security implications considered
- [ ] Performance implications considered
- [ ] Rollback strategy exists (if applicable)
### Actionability
- [ ] File paths are specific
- [ ] Verification commands are copy-pasteable
- [ ] Existing code to read is referenced
- [ ] Conventions and patterns are noted
### Proposed Code Quality *(if plan includes implementation details)*
- [ ] Proposed types/signatures handle edge cases
- [ ] Proposed names follow project conventions
- [ ] Proposed abstractions follow existing patterns
- [ ] No security vectors in proposed APIs
- [ ] No performance issues in proposed structures
### Verdict
- [ ] **Approve** — Ready to implement
- [ ] **Request changes** — Gaps must be addressed
```
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "I'll figure out the details during implementation" | That's how you discover blocking dependencies mid-task. Surface them now. |
| "The tasks are obvious, no need for criteria" | Write them anyway. Explicit criteria surface hidden assumptions. |
| "It's just a small feature, it doesn't need a plan" | Small features have edge cases too. 3 tasks with criteria takes 5 minutes. |
| "The plan is good enough" | "Good enough" without acceptance criteria means the implementer defines "done" — and they might define it differently. |
| "I'll add verification steps later" | Later never comes. The plan is the contract — define verification now. |
| "Risks are minimal" | Every change has risks. If you can't name them, you haven't thought about them. |
| "The file paths are obvious" | They're obvious to the author. The implementer might not know the codebase. |
| "The code in the plan is fine, it'll get reviewed later" | Plan-level code review catches design problems before implementation — fixing them after coding is more expensive. |
## Red Flags
- No acceptance criteria on any task
- Tasks that say "implement the feature" without specifics
- No verification steps anywhere in the plan
- All tasks are XL-sized
- No checkpoints between phases
- Dependency order isn't considered (e.g., API handler before domain model)
- No testing strategy
- Verification commands are generic ("run tests") instead of project-specific
- Plan has 20+ tasks (scope too large for one plan)
- No risk section on a plan with migrations, breaking changes, or security implications
- Horizontal slicing (all domain, then all services, then all API)
- File paths are vague ("update the relevant files")
- Missing open questions section despite stated unknowns
- Proposed code ignores project conventions or existing patterns
- Proposed types use gratuitous `any`/`unknown`/optional without justification
- Proposed APIs don't validate input at boundaries
## See Also
- For producing plans, use the `planner` skill
- For reviewing implemented code, use `code-review` — also the criteria source for axis 6
- For security-specific concerns, see `security-and-hardening`
- For testing strategy guidance, see `testing`

View File

@ -1,78 +0,0 @@
---
name: ste
description: Write or rewrite text in ASD-STE100 Simplified Technical English. ONLY use this skill when the user explicitly invokes it by name — i.e. they type "/ste" or literally write "use the ste skill" / "apply ASD-STE100". Do NOT trigger it on paraphrased intent such as "simplify this", "make it clearer", "write technical documentation", or "shorter sentences please" — the user has deliberately scoped this skill to explicit invocation only. For those requests, respond normally without loading this skill unless they name it.
---
# ASD-STE100 Simplified Technical English
Apply the ASD-STE100 standard to all prose you produce in this task. Do not announce that you use STE, do not name the standard, and do not explain the style unless the user asks. If the user later asks you to "write more naturally," ask one short question to confirm they want to leave STE before you drop it.
Compliance note (for you, not for output): the official specification and its dictionary are copyright ASD. This skill encodes paraphrased rules and a publicly sourced word list. For certified aerospace/defense deliverables, tell the user that full compliance requires the free official specification (asd-ste100.org) and a human sign-off. Never claim certified compliance.
## Step 0 — Classify the text
Before writing a single sentence, decide: is this **procedural** text (instructions someone follows) or **descriptive** text (explanation, background, description)? Every limit below depends on this. Mixed documents get classified section by section.
## Core rules
### Sentences
- Procedural: maximum **20 words** per sentence.
- Descriptive: maximum **25 words** per sentence.
- Maximum **6 sentences** per paragraph. One topic per paragraph.
- One instruction per sentence. Two actions in one sentence only if they occur at the same time.
- Put a condition BEFORE its command: "If the pressure decreases, close the valve."
- Do not omit articles, subjects, or verbs to save words. "Ensure file exists" is wrong; "Make sure that the file exists" is correct. Keep the word "that" after verbs like "make sure."
- Numbers, units with numbers, abbreviations, quoted strings, code identifiers, and proper nouns each count as one word.
### Verbs
- Allowed forms only: infinitive, imperative, simple present, simple past, simple future, and past participle used as an adjective.
- Never use present perfect or continuous forms. "We have received" → "We received." "is being tested" → a simple form.
- Never use an -ing form as a verb. An -ing word is allowed only inside a technical name ("the mounting bracket," "logging").
- Active voice. Passive is allowed only in descriptive text when the agent is unknown or unimportant.
- Instructions use the imperative: "Open the panel," not "You must open the panel" or "The panel should be opened."
- Express actions as verbs, not nouns: "compress the file," not "perform compression of the file."
- Modals: use **can** (possibility), **will** (future), **must** (requirement). Do not use should, would, could, may, might. A hedge becomes a fact or a "can": "an explosion can occur."
- No phrasal verbs: "go down" → "decrease," "set up" → "install," "carry out" → "do."
### Words
- One word, one meaning, one part of speech, used consistently. Never rotate synonyms: pick one name for a thing and repeat it.
- Before drafting, replace unapproved vocabulary. Read `references/word-substitutions.md` and apply it; it is the working dictionary for this skill.
- Domain-specific nouns (part names, tool names, product names, UI labels) and domain verbs (drill, ream, boot, compile) are your **technical nouns/verbs** — keep them as-is, use each consistently, and do not verb a noun or noun a verb.
- Noun clusters: maximum **3 words** ("overhead panel light" is the limit). Longer clusters get decomposed with prepositions or hyphenated on first use: "main-gear-door retraction-winch handle."
- American English spelling.
- No Latin abbreviations: "e.g." → "for example," "i.e." → "that is," delete "etc."
### Punctuation
- No semicolons — write two sentences.
- Parentheses only for references, abbreviations, and item numbers.
- Hyphenate words that act as one unit; a hyphenated word counts as one word.
- No contractions.
### Warnings, cautions, notes
- **WARNING** = risk of injury or death. **CAUTION** = risk of damage. **NOTE** = information only, never an instruction.
- Start a warning or caution with the command or condition, then give the risk:
"WARNING: Do not touch the terminal. The terminal has a dangerous voltage."
- Notes obey the 25-word descriptive limit.
## Step 2 — Self-check pass
After drafting, scan your text once for each of these and fix every hit before you respond:
1. Any sentence over the 20/25-word limit for its type
2. Contractions, semicolons
3. "should," "would," "could," "may," "might"
4. "has been," "have been," "had been," "is being," "was being"
5. -ing words used as verbs
6. Missing articles (a/an/the/this) before nouns
7. Synonym rotation (the same object under two names)
8. Any word in the unapproved column of `references/word-substitutions.md`
9. Warnings that state the risk before the command
## Reference files
- `references/word-substitutions.md` — unapproved → approved word mappings and one-meaning rulings. Read it before drafting; it is short.
- `references/examples.md` — worked before/after rewrites (procedural, descriptive, warnings, common mistakes). Read it when rewriting existing text or when unsure how a rule applies.
## What NOT to touch
Code blocks, command strings, file paths, error messages, quoted UI text, and proper nouns stay exactly as written. STE applies to the prose around them.

View File

@ -1,67 +0,0 @@
# Worked before/after examples
## Verb forms
| Before | After |
|---|---|
| We have received the technical reports from HQ. | We received the technical reports from HQ. |
| This device has been being used at Boeing since 2005. | Boeing started to use this device in 2005. |
| The test is continued by the operator. | Continue the test. |
| The screws should be replaced. | Replace the screws. |
| The system is currently running diagnostics. | The system does diagnostic tests now. |
## Vocabulary and phrasing
| Before | After |
|---|---|
| Ensure file exists before running. | Make sure that the file exists before you run the command. |
| Rotate the cover until the jacks are accessible. | Turn the cover until you can get access to the jacks. |
| Extend the jack until the wheels are clear of the ground. | Extend the jack until the wheels do not touch the ground. |
| Make sure that these steps are followed. | Obey these steps. |
| Utilize approximately 3 liters of water. | Use about 3 liters of water. |
| Perform a compression of the log files prior to upload. | Compress the log files before you upload them. |
## Noun clusters
| Before | After |
|---|---|
| Main gear door retraction winch handle | Main-gear-door retraction-winch handle |
| Runway light connection resistance calibration | Calibration of the resistance of the runway light connection |
| Remove the engine transmission housing attachment bolts. | Remove the bolts that attach the transmission housing to the engine. |
## Procedural rewrite (condition first, one instruction per sentence)
Before:
> You should check that the reservoir has been filled with the correct fluid and, after attaching a clear tube to the bleed screw while ensuring its free end is submerged, the pedal can be pumped and held so that opening the screw releases trapped air.
After:
> 1. Fill the reservoir with the correct fluid.
> 2. Attach a clear tube to the bleed screw.
> 3. Put the free end of the tube in a container of fluid.
> 4. Push the pedal three times. Hold the pedal down.
> 5. Open the bleed screw one half turn. Air and fluid flow into the tube.
> 6. Close the bleed screw. Release the pedal.
> 7. If air continues to come out, do steps 4 thru 6 again.
## Warnings and cautions (command first, then risk)
Before:
> Note that serious data loss may potentially occur if the --force flag is used against production.
After:
> CAUTION: Do not use the --force flag on the production database. The flag deletes the rows that do not match the source.
Before:
> Touching the terminal could result in electrocution.
After:
> WARNING: Do not touch the terminal. The terminal has a dangerous voltage.
## Common mistakes checklist
- Dropped articles: "Insert pin in bracket" → "Insert the pin in the bracket."
- Synonym rotation: check/verify/confirm for the same action → one term, everywhere.
- Hedges: "you may want to," "it is recommended that" → an imperative or "must."
- Instruction buried in a NOTE: notes never instruct. Move the instruction to a numbered step.
- Semicolon joining two clauses → two sentences.
- "There are three bolts on the panel" → "The panel has three bolts."

View File

@ -1,68 +0,0 @@
# Word substitutions and one-meaning rulings
Compiled from public secondary sources (STEMG/ASD public pages, TechScribe, Acrolinx, training materials). This is a working approximation, not the official ASD dictionary. When a word is not listed here and feels formal or Latin-derived, prefer the shortest common alternative.
## Unapproved → approved
| Do not use | Use instead |
|---|---|
| utilize, leverage, employ | use |
| commence, initiate, begin, originate | start |
| terminate, cease, conclude | stop, end |
| ensure, verify, confirm, validate, check | make sure (that), examine |
| perform, conduct, execute, carry out | do |
| facilitate, assist | help |
| obtain, acquire, procure | get |
| sufficient, adequate | enough |
| approximately | about |
| prior to | before |
| subsequent to, following (prep.) | after |
| adjacent to | near |
| accomplish | do |
| additional, supplementary | more |
| attempt | try |
| require, necessitate | need, must |
| mandatory | necessary |
| indicate, signify | show |
| observe (=watch) | look at, examine |
| rotate | turn |
| deactivate | turn off, set to off |
| activate, energize (unless technical verb) | turn on, start |
| toxic | poisonous |
| in order to | to |
| via, by means of | through, with |
| due to, owing to | because of |
| in the event of/that | if |
| accessible | (rewrite: "you can get access to") |
| remainder | rest |
| demonstrate | show |
| modify, alter | change |
| construct, fabricate, build | assemble, make |
| retain | keep |
| locate (=find) | find |
| depress (a button) | push, press |
| proceed | continue, go |
## One meaning, one part of speech (canonical rulings)
- **close** — verb only: to move to a position that stops flow, or to operate a circuit breaker. The adjective is unapproved → use **near** ("do not go near the propeller").
- **test** — noun only: "do a test," never "test the system."
- **check** — do not use as a verb for verification → "make sure that" or "examine."
- **follow** — means only "come after." For rules and steps use **obey**: "Obey the safety instructions."
- **fall** — means only "move down by gravity." For quantities use **decrease**. Never the season.
- **oil** — noun only. "Oil the bearing" → "Put oil on the bearing" / "Lubricate the bearing."
- **right** — direction only, never "correct."
- **clear** — "without blockage." "Wheels are clear of the ground" → "wheels do not touch the ground."
- **help** — verb only; the noun is **aid** ("with the aid of a mirror").
- **above / below** — physical position only. For quantities: **more than / less than**.
- **about** — two approved senses: "approximately" and "on the subject of." Use carefully.
- **turn** — the general verb for rotation; "turn on / turn off" for power state is standard.
- **level** — approved as noun and adjective (documented exception to the one-POS rule).
## Frequent-offender function words
- **should / would / could / may / might** — never. Requirement → **must**. Possibility → **can**. Future → **will**.
- **etc.** — delete, or write the full list.
- **e.g. / i.e.** — "for example" / "that is."
- **any / appropriate / applicable / relevant** as hedges — replace with the specific thing meant.
- **there is / there are** openers — rewrite with a real subject: "There are three bolts on the panel" → "The panel has three bolts."

View File

@ -1,57 +0,0 @@
---
name: testing
description: Enforce TDD workflow and testing best practices for Penpot. Use when implementing features, fixing bugs, or modifying behavior. Reads testing memory for full guidance.
---
# Testing Skill
Enforces test-driven development and Penpot testing conventions.
## When to Use
- Implementing new logic or behavior
- Fixing any bug (reproduction test required)
- Modifying existing functionality
- Adding edge case handling
**Skip:** Pure configuration changes, documentation updates, or static content with no behavioral impact.
## Workflow
Follow TDD (Red → Green → Refactor) whenever practical:
1. **RED** — Write a failing test first
2. **GREEN** — Write minimal code to pass
3. **REFACTOR** — Clean up while tests stay green
For bug fixes, use the Prove-It Pattern: write a test that reproduces the bug, confirm it fails, implement the fix, confirm it passes.
## Required Reading
Before writing any test, read:
1. `.serena/memories/testing.md` — cross-cutting testing principles, TDD workflow, anti-patterns, execution discipline
2. Module-specific testing memory for the affected module:
- `mem:common/testing` — CLJC unit tests
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E
- `mem:backend/core` — JVM clojure.test conventions
## Key Rules
- Every behavior change needs a test
- Test state, not interactions
- DAMP over DRY — tests are specifications; duplication is OK if each test is self-contained and readable
- Prefer Real > Fake > Stub > Mock
- Arrange-Act-Assert structure
- One assertion per concept
- Never pipe test output to filters — redirect to file first
- Register new test files in the module's runner/entrypoint
## Verification
After completing implementation:
- [ ] Every new behavior has a test
- [ ] All tests pass for touched modules
- [ ] Bug fixes include a reproduction test
- [ ] Lint/formatter passes

View File

@ -5,8 +5,7 @@ Backend: JVM Clojure; Integrant; PostgreSQL; Redis/Valkey; RPC; HTTP; storage; m
## Focused memories
- RPC, DB helpers, workers, cron: `mem:backend/rpc-db-worker-subtleties`
- Storage abstraction, logical buckets, object lifecycle, deduplication, access, and garbage collection: `mem:backend/storage`.
- HTTP sessions, config, media processing, and file data persistence: `mem:backend/http-storage-filedata-subtleties`.
- HTTP sessions, config, storage, media, file data persistence: `mem:backend/http-storage-filedata-subtleties`
- Auth flows, permission model, teams, projects, invitations, comments, webhooks, audit: `mem:backend/auth-permissions-product-domains`
- Services, task-queue/Pub-Sub topology constraints -> `mem:prod-infra/core`.
@ -93,8 +92,8 @@ Fixtures can populate local data for manual testing/perf work. From the backend
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
* **Linting:** `pnpm run lint:clj`.
* **Formatting:** `pnpm run check-fmt:clj` to check, `pnpm run fmt:clj` to fix. After running `fmt:clj`, `check-fmt:clj` is redundant. Avoid unrelated whitespace diffs.
* **Linting:** `clj-kondo --lint ../common/src/ src/`.
* **Formatting:** `cljfmt check src/ test/` to check, `cljfmt fix src/ test/` to fix. Avoid unrelated whitespace diffs.
**Before linting:** if delimiter errors are suspected (after LLM edits), run
`scripts/paren-repair` on the affected files first. Delimiter errors produce
@ -108,3 +107,4 @@ IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. J
* **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace.
* **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas.
* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.

View File

@ -14,7 +14,10 @@
## Storage and media
- Storage abstraction, backend configuration, logical buckets, object lifecycle, deduplication, access rules, and garbage collection: `mem:backend/storage`.
- Storage has a fixed valid bucket set. Backends are `:fs` and `:s3`; default backend comes from deprecated `assets-storage-backend` only when present, otherwise `objects-storage-backend`, defaulting to `:fs`.
- `put-object!` creates the DB `storage_object` row before writing backend content. Backend writes happen only for newly created rows, so deduplication can skip object writes.
- Deduplication only applies when requested, when the content can provide a hash, and when bucket metadata is present. Reads exclude soft-deleted storage rows.
- `sto/resolve` can reuse the current DB connection via `::db/reuse-conn true`; preserve this in transaction-sensitive code.
- SVG validation strips DOCTYPE and uses secure SAX parsing. Basic SVG info falls back to 100x100 dimensions when width/height/viewBox are missing.
- Raster metadata is shell-derived with ImageMagick `identify`, verifies detected MIME against the supplied MIME, and swaps dimensions for EXIF orientations 6/8.
- Remote image download requires 2xx status, `content-length`, a known MIME, and size under the configured maximum before writing the temp file; mismatched byte count is an internal error.

View File

@ -1,83 +0,0 @@
# Backend Storage
## Abstraction
- `app.storage` stores binary objects.
- Each object has a `storage_object` database row.
- The row stores the UUID, size, backend, timestamps, and Transit metadata.
- The backend stores the binary content.
- Supported backends are `:fs` and `:s3`.
- FS uses one root directory and a UUID-derived path.
- S3 uses one configured bucket and an optional prefix.
- A Penpot bucket is metadata. It is not an S3 bucket or a filesystem directory.
- FS and S3 use the same UUID-derived object path. The bucket does not change the path.
- `PENPOT_OBJECTS_STORAGE_*` configures the current object backend.
- Deprecated asset-storage config keys remain supported for migration.
- Database rows keep the backend name. Keep the legacy `:assets-fs` and `:assets-s3` aliases.
## Object Lifecycle
- `put-object!` creates the database row before it writes backend content.
- Backend content is written only when the row is new.
- A failed backend write can leave an unreferenced database row.
- Callers often set `:touched-at` so garbage collection can remove such rows.
- `get-object` excludes rows with `deleted_at`.
- Existing object values can remain readable until physical deletion.
- `:expired-at` blocks reads after the expiration time.
- `del-object!` sets `deleted_at`. It does not remove backend content.
- `storage-gc-deleted` removes the database row and backend content after the deletion delay.
- `storage-gc-touched` finds references before it sets `deleted_at`.
- `objects-gc` removes deleted domain rows and touches their storage object IDs.
- Use `::db/reuse-conn true` with `sto/resolve` inside a database transaction.
## Deduplication
- Deduplication requires `::sto/deduplicate?`, a content hash, and bucket metadata.
- The lookup matches hash, bucket, backend, and `deleted_at IS NULL`.
- The lookup does not include file ID, profile ID, team ID, or organization ID.
- Objects can therefore share content across users and files within one bucket.
- Deleted objects are not reused.
- `tempfile` objects never use deduplication, even when the caller requests it.
- Use `sto/wrap-with-hash` when the caller already calculated the content hash.
## Bucket Rules
| Bucket | Content and references | Dedup | Direct `/assets/by-id` access | Cleanup |
| --- | --- | --- | --- | --- |
| `file-media-object` | Original file images and generated media thumbnails. References: `file_media_object.media_id` and `thumbnail_id`. | Yes | Public | Reference scan. |
| `team-font-variant` | Font variants in `team_font_variant`. References: `woff1_file_id`, `woff2_file_id`, `otf_file_id`, and `ttf_file_id`. | Yes | Public | Reference scan. |
| `file-object-thumbnail` | Frame and component thumbnails in `file_tagged_object_thumbnail.media_id`. | Yes | Public | Reference scan. |
| `file-thumbnail` | File grid thumbnails in `file_thumbnail.media_id`. | Yes | Authentication required | Reference scan. |
| `profile` | User and team profile photos. References: `profile.photo_id` and `team.photo_id`. | Yes | Authentication required | Reference scan. |
| `organization` | Organization logos uploaded by the Nitrate management API. | Yes | Public | No reference scan. A touched object is deleted. |
| `tempfile` | Export files, chunked-upload chunks, and temporary font downloads. | No | Authentication required | No reference scan. A touched object uses a two-hour deletion delay. |
| `file-data` | Encoded file data when `file-data-backend` is `storage`. Reference metadata has `storage-ref-id`, `file-id`, and the `file_data` row ID. | Yes | Authentication required | Reference scan. |
| `file-data-fragment` | Compatibility value for file-data fragments. The current backend has no dedicated producer for this bucket. | No current write semantics | Public | No touched-object collector case. |
| `file-change` | Compatibility value for file changes. Current snapshots store data in `file_data`, not this bucket. | No current write semantics | Authentication required | No touched-object collector case. |
- The valid bucket set lives in `app.storage/valid-buckets`.
- `file-media-object` is the default bucket for old rows without bucket metadata.
- Do not assign a new bucket without adding its access and cleanup behavior.
- The touched-object collector raises an internal error for an unknown bucket.
- It supports `file-media-object`, `team-font-variant`, `file-object-thumbnail`, `file-thumbnail`, `profile`, `file-data`, `tempfile`, and `organization`.
- It does not support `file-data-fragment` or `file-change`.
## Access Rules
- `app.http.assets` decides direct object authentication from the bucket.
- Public buckets are `file-media-object`, `file-object-thumbnail`, `team-font-variant`, `file-data-fragment`, and `organization`.
- Other valid buckets require a session or access-token profile ID.
- File-media routes also require file read permission.
- Non-public direct responses set `content-disposition: attachment`.
- FS responses use `x-accel-redirect` for the configured asset path.
- S3 responses use a presigned URL and an HTTP redirect.
## File Data
- `file-data-backend` accepts `legacy-db`, `db`, or `storage`.
- `legacy-db` stores main data in `file.data` and snapshots in `file_change.data`.
- `db` stores encoded data in `file_data.data`.
- `storage` stores encoded data in storage subsystem with `file-data` bucket and keeps `data` nil in `file_data` table.
- The `file_data.metadata.storage-ref-id` value points to the storage object.
- `fdata/upsert!` touches a storage object from incoming metadata before it stores the new row.
- File snapshots use `file_data` for snapshot data and `file_change` for snapshot metadata.

View File

@ -24,12 +24,6 @@ Variant masters are main instances and component roots. Their descendants may th
Masters are not normally touched through `set-shape-attr`, but touched flags can appear on master shapes through cloning/duplication paths. `add-touched-from-ref-chain` in `app.common.logic.variants` unions touched flags from ancestors into the copy being processed, so upstream/master touched state can affect downstream switch behavior.
## Swap slots and positional matching
- A swap slot (stored via `ctk/set-swap-slot`, a `:touched` group `swap-slot-<uuid>`) marks a copy sub-head that was SWAPPED to another component; `compare-children` then pairs it to the main child by slot instead of by `shape-ref`.
- Copy sub-heads without a slot are paired to main children by `shape-ref` (seek, not index). `find-near-match` (positional) is only a validator/repair heuristic; validity requires membership of the ref among the near-main parent's children, not index equality (`mem:common/file-change-validation-migration-subtleties`).
- Copy child ORDER converges to the main's via the async sync (`moved` branch of `compare-children`); local code must never reorder copy children directly (guards in `:mov-objects`/`:reorder-children`).
## Cloning paths
`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:variant-id`, and `:variant-name` on cloned shapes.

View File

@ -5,7 +5,7 @@
## Stable namespace map
- `app.common.data` and `app.common.data.macros`: generic data helpers and performance macros that do not depend on Penpot domain entities.
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.organization` contains organization schemas, `apply-organization`, and fail-closed organization/team permission rules (`allowed?`, `can-send-invitations?`).
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.nitrate-permissions` contains shared fail-closed Nitrate organization/team permission rules.
- `app.common.files.*`: file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic.
- `app.common.logic.*`: higher-level workflows/algorithms over files, shapes, components, variants, libraries, tokens, etc.
- `app.common.geom.*`: geometry helpers and transformations.

View File

@ -7,8 +7,6 @@
- `set-shape-attr` treats `:position-data` as derived and never touched. Geometry/content-path changes use approximate equality; geometry differences under about 1px can be ignored for touched purposes.
- Width/height are excluded from the `is-geometry?` branch in `set-shape-attr`; do not assume all geometry-group attrs follow identical ignore-geometry behavior.
- `process-touched-change` marks the owning component modified when a touched shape belongs to a main instance; component-data changes can come from shape ops through this second pass.
- Copy structure is guarded at change application: `:mov-objects` (`is-valid-move?`) and `:reorder-children` both refuse to alter children of shapes inside component copies unless the change carries `allow-altering-copies` (sync/swap flows set it). New structural change types must follow the same rule.
- `cls/generate-delete-shapes` propagates deletions from INSIDE a component main to the copy shapes referencing them (transitively, all pages of the file) so no dangling `shape-ref`s remain; skipped when the main root itself is deleted (copies then resolve into the deleted component) and for `allow-altering-copies` flows (swap replaces the shape; sync reconciles).
## Shape tree edits
@ -21,7 +19,6 @@
- Full referential/semantic validation currently runs only when file features contain `"components/v2"`.
- Validation starts at root plus orphan shapes, then validates component records. `validate-file!` raises `:validation :referential-integrity` with collected details.
- `repair-file` does not mutate data directly; it reduces validation errors into redo changes using `changes-builder`. Callers must apply or persist those changes.
- `:missing-slot` fires only for a REAL swap: a copy sub-head whose `shape-ref` is no longer a child of the near main parent. A pure positional mismatch (ref still a sibling elsewhere) is a reorder — valid, realigned by the async component sync; do not "repair" it by assigning swap slots (a slot freezes the child out of normal sync). `fix-missing-swap-slots` (migration 0019) follows the same membership rule.
## Migrations

View File

@ -8,9 +8,6 @@
## Grid assignment
- Grid `assign-cells` ensures at least one column and row, skips absolute-position children, creates non-tracked rows/cols when children exceed tracked cells, and asserts that assigned cells do not overlap.
- `position-absolute?` counts HIDDEN shapes as absolute: hiding a grid child frees its cell on the next `assign-cells`.
- `reorder-grid-children` rewrites the parent's `:shapes` to the REVERSE of the sorted cell order, but children with no cell (hidden/absolute) keep their original index — do not "fix" this into moving them to an end; that broke copy/main positional slot alignment (referential-integrity crash).
- The `:reorder-children` change it emits is refused on parents inside component copies unless `allow-altering-copies` (same rule as `:mov-objects`); `pcb/reorder-grid-children` also skips copy grids producer-side. Copy child order is owned by the component sync engine.
- Grid deassignment removes cells for shapes that are no longer direct children or have become absolute-positioned.
- Auto-positioning is not just sorting: some auto cells are converted to manual when empty/manual/span state would break the auto sequence, then auto single-span items can be compacted.
- `fix-overlaps` is marked dev-only and removes one overlapping cell, preferring empty cells first. Avoid depending on it as normal production repair.

View File

@ -39,7 +39,6 @@ This is a monorepo. Principles that apply to one module do *not* generally apply
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types; core conventions: `mem:plugins/core`.
- `library/`: design library workflows; core conventions: `mem:library/core`.
- `docs/`: documentation site; core workflow and conventions: `mem:docs/core`.
- `media-processor/`: TypeScript/Node.js HTTP service for image (sharp) and font (FontForge) processing; core conventions: `mem:media-processor/core`.
The memory is structured in a way that you can get the critical information about the
module. You can read it from `mem:<MODULE>/core`

View File

@ -25,9 +25,7 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
## Worker policy
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
Each workspace is independent and can be started/stopped in any order. Shared infra (postgres, minio, etc.) is shut down only when no instances remain running.
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. ws0 must be running whenever any ws1+ is running, and is the last instance to stop — `run-devenv --agentic --ws N` (N≥1) auto-starts ws0 first; `stop-devenv` refuses to stop ws0 while any ws1+ is up. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
## Port layout
@ -65,8 +63,8 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
## CLI surface
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet).
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra.
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). Auto-starts ws0 first when the target is ws1+ and ws0 is not yet up.
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` (N≥1) stops just that workspace. `--ws 0` or no flag stops ws0 + shared infra, refused while any ws1+ is running. `--all` stops every ws highest-first then ws0, then infra.
- `run-devenv`: legacy alias, ws0 non-agentic attached.
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
- `run-devenv-shell [--instance 0|wsN|N] [cmd...]`: bash in target instance. (`--instance` flag not yet renamed to `--ws`.)

View File

@ -5,10 +5,9 @@
## Layout and commands
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; test bundle `pnpm run build:test`; tests `pnpm run test` or `pnpm run test:quiet`; lint `pnpm run lint:clj`; format check/fix `pnpm run check-fmt:clj` / `pnpm run fmt:clj`.
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Exporter test conventions and CI: `mem:exporter/testing`.
## HTTP and browser pool

View File

@ -1,16 +0,0 @@
# Exporter Testing
- READ `mem:testing` first.
- Tests use `cljs.test` and live under `exporter/test/exporter_tests/`.
- Register every test namespace in `exporter-tests.runner`.
- From `exporter/`: `pnpm run build:test` builds the Node test bundle without running tests.
- From `exporter/`: `pnpm run test` builds and runs tests with full output.
- From `exporter/`: `pnpm run test:quiet` builds and runs tests with reduced build output.
- After `build:test`, reuse the compiled bundle with `node target/tests/test.js`.
- For iterative focused runs, build once and reuse the compiled bundle.
- Focus a test namespace with `node target/tests/test.js --focus exporter-tests.renderer-svg-test`.
- Focus a test var with `node target/tests/test.js --focus exporter-tests.renderer-svg-test/creates-the-correct-gradient-element`.
- Set app log level by appending `--log-level warn` (or `trace|debug|info|warn|error`).
- `test:quiet` accepts forwarded options but rebuilds the bundle; prefer the direct runner after `build:test` for focused runs.
- From `exporter/`: `pnpm run check-fmt:clj` checks ClojureScript formatting.
- From `exporter/`: `pnpm run lint:clj` runs ClojureScript linting.

View File

@ -1,152 +0,0 @@
# Composable component tests
A framework concept for systematically testing Penpot's component subsystem
(synchronisation/propagation, swaps, variant switches, nesting, overrides), implemented in TWO test
suites that share the principles below:
1. **ClojureScript suite** — in the frontend test tree (`frontend/test/frontend_tests/
composable_tests/`), driving a minimally-assembled real app headlessly. The original.
2. **TypeScript suite** — a Penpot plugin (`plugins/apps/composable-test-suite/`), driving the FULL
production app end-to-end through the Plugin API, with a slightly more elaborate set of
abstractions. Runs interactively (panel), remotely (Playwright), and headlessly in CI. Its
README is the authoritative operational reference.
## Shared core idea
A test is a **composition of operations** over a starting configuration, plus assertions. You
describe a test as data (a setup + a sequence of operations) rather than writing bespoke imperative
code, and coverage grows by COMPOSITION: a new variation is one combinator wrapped around existing
pieces, not a copied test. Choice points (one-of alternatives, optional steps) EXPAND the
composition into a full sweep of variants — one written case stands for a whole matrix of concrete
tests.
## Shared principles
- **Every producing object is the accessor interface to what it produces downstream.** An
operation — and related objects such as content-creation strategies — is not merely an action:
the SAME object instance the case holds is the typed interface through which everything it
created or changed is later retrieved, checked, and asserted, parameterized by the situation. A
foundation operation exposes accessors for the participants it built; an edit operation exposes
its dual check (`assertHasChangedProperty` / `has-property-of`); a choice is recovered by asking
the one-of object (`getChoice`/`get-choice`); "did this step run" is asked of the step
(`wasApplied`/`applied?`). NEVER reach into a situation (or the document) for something an
upstream object produced — ask the producer. This is what keeps sweeps sound (object identity
ties the question to the exact node that ran) and what keeps retrieval logic in exactly one
place. Particularly explicit in the TS OOP implementation, where these accessors are methods on
the operation/strategy classes; repeatedly violating it (reading the document directly,
duplicating retrieval) was the most common review correction while building the suites.
- **Operations are data with identity.** Each operation node has a unique id at construction and
records what it did under that id; interrogation is by identity. Bind an operation to a value
ONCE and reuse it in the composition and in every query about it.
- **Drive the real production pipeline.** Operations route through genuine Penpot logic — real
change functions / real workspace events / the real Plugin API, never raw field writes — so the
production watcher's AUTOMATIC propagation is what's under test.
- **Roles, not internals.** A starting configuration names its participants (roles). Role→id
capture happens when the configuration is built; operation TARGETS resolve at apply-time and may
be re-bound, so an operation targeting a role follows it as state-building ops re-point it —
which lets a single operation be swept across depth.
- **Enumeration is authored, not exhaustive.** Compose only VALID cases, so outcomes are just
pass / fail / error — no not-applicable cells.
- **Naming discipline.** Penpot domain nouns ("component", "variant") must not name framework
abstractions; an operation may name the domain ACTION it performs.
- **Operator algebra** (same in both suites): sequence (cartesian product of the steps' variants),
one-of (union, choice recorded), optional(X) = one-of([X, skip]), inline assertion ops, trailing
asserters.
- **Case authoring:** a case carries a CamelCase identifier and a plain-terms description in three
parts — situation setup, actions/variations, asserted requirement.
---
# ClojureScript suite (frontend test tree)
Test-only `.cljs` code in the frontend test tree (nothing "common" about it). A **situation** =
the in-memory file value + named roles + `:vars` + an ordered applied-log. Operations are records
implementing `IOperation`/`apply-to` (`apply` collides with core). Assertions = inline `Test` ops
and/or a trailing asserter; the runner makes no judgment. Failures carry `describe-applied` (the
transcript), which is what makes a failing variant in a sweep identifiable.
Layout: `core.cljs` (the domain-agnostic engine: situation, identity/transcript, roles/targets,
operators, runners), `comp/setups.cljs` (setups + role accessors), `comp/nodes.cljs` (the component
operations and their check duals), `interpreter.cljs` (runs cases against the real frontend),
`comp/sync_test.cljs` (the cases; registered in `frontend_tests/runner.cljs`). Case letters B..N;
the sweeps (K: depth × edit-precedence; L: swaps; M: variant switches; N: rotated-instance
geometry, on the #10109 fix branch until merged) are the flagship pattern — read them before
writing a new sweep.
**Scenario lineage model** (behind the sweeps): scenario ops track named component lineages as
objects under `:vars`, each holding the FIXED deepest origin (`:remote-*`), the ADVANCING outer
main (`:main-*`), and per-nesting-level data whose `:nested-head` (the deepest instance at that
level, found by descending the `:shape-ref` chain — matching chain MEMBERSHIP, not terminus) is
the swap/switch target, anchored by its swap-stable parent. Nesting seeks the FIXED origin, not
the advancing main — that is what makes each level's `:nested-head` land on the deepest instance.
A variant nesting re-points the lineage's remote to the chosen member. Construction lesson:
cross-level propagation requires progressively NESTED levels (one variant + plain wraps); sibling
nestings do not propagate between each other.
**Interpreter:** installs the situation's files into the global `st/state` (aux files tagged
`:library-of`), starts the real `watch-component-changes` (+ harness `watch-undo-stack`), maps
event-ops to REAL workspace events (`dwsh/update-shapes`, `dwl/component-swap`,
`dwv/variants-switch`, `dwt/increase-rotation` — which runs the `check-delta` placement
classification — `dwt/update-dimensions`, `dwu/undo`, `dwl/sync-file`, …) and runs sync-ops'
`apply-to` against the live store file; awaits settlement (idle-gap heuristic + per-op grace) and
re-reads `:file` each step so the shared accessors keep working.
STORE-SWAP IMMUNITY: other test namespaces `set!` `st/state`/`st/stream` and never restore, while
the `app.main.refs` lenses stay bound to the ORIGINAL atoms — propagation then dies silently. The
interpreter captures the atoms at namespace-load time and re-`set!`s them per variant.
Running: `cd frontend && pnpm run build:test`, then
`node target/tests/test.js --focus frontend-tests.composable-tests.comp.sync-test`
(var-level focus for one case).
**Fidelity warning:** the harness drives a MINIMALLY-ASSEMBLED app — only some
`initialize-workspace` subscriptions are wired. Risk = SILENT UNDER-WIRING (e.g. undo needs the
harness `watch-undo-stack`). When a case needs app behaviour beyond a raw edit, check for an
unwired subscription and verify by PROBING store state, not by trusting a green assertion.
**Caveats:** inline `Test` exceptions are UNCAUGHT on the frontend (crash the runner — assert in
the trailing asserter). `(optional (in-sequence …))` is not flattened for the interpreter — use
independent optionals. The Serena/clj-kondo cache for `nodes.cljs` goes stale (phantom symbols) —
trust the build. Cross-namespace global-state leaks land in this suite first; suspect them before
the framework on inexplicable full-run-only failures. Case H's `sync-file` schedules a delayed RPC
that fails headless (benign; absorbed by per-op grace).
---
# TypeScript suite (the plugin) — full e2e
`plugins/apps/composable-test-suite/` — same principles against the FULL production app through the
Plugin API (real frontend, real propagation). Continuation of the CLJS suite per issue #10584.
Operational details (build/run, connect URL, remote control, reading logs, auto-reload, CI): the
plugin README.
Distinguishing abstractions (the OOP articulation of the shared principles):
- `TestCase {identifier, description, operation}` with the three-part description mandated in the
constructor docstring.
- The accessor-interface principle is class-level: foundation operations (e.g.
`OpCreateSimpleComponentWithCopy`) expose the roles they build; **content-creation strategies**
(pluggable: what content a foundation builds around) expose accessors for the content they
created; edit operations expose their checks (`OpChangeProperty.assertHasChangedProperty`);
`OpOneOf`/`OpOptional` are queried for what ran. Tests never grope the document for something a
producer can be asked for.
- `ShapeProp` model: property duals with numeric tolerance; rotation is a writable attr, height
goes via resize (readonly in the Plugin API).
- `TestSuite` enumerates cases into a `TestTree` with stable per-test ids;
`run(ids, TestRunObserver)` is the ONLY output channel — the framework is UI-free by
construction. `plugin.ts` (panel adapter), `main.ts` (panel UI) and `src/ci/headless.ts`
(CI adapter) are three thin consumers.
- Cases live in `src/composable-tests/cases/` as `case<Identifier>.ts` (e.g. `MainEditSyncs` — the
sweep that found #10109).
- Panel checkboxes carry stable DOM ids (case identifier / `Identifier-N` composites) for remote
control via Playwright; recipe in the README.
## CI
Headless per-PR gate: `.github/workflows/tests-composable-suite.yml` runs
`pnpm --filter composable-test-suite run test:ci` — mocked backend (frontend e2e static server +
Playwright RPC fixtures, no backend/login), the in-sandbox bundle injected via `ɵloadPlugin`,
results streamed via console markers, `TEST_FILTER` by identifier substring. The mocked backend is
NOT a limitation for this suite (everything asserted is frontend store logic; empirically
confirmed against the interactive runs). Architecture mirrors `plugin-api-test-suite`'s CI driver;
the mock harness exists in THREE places that must stay in sync (provenance note in `ci/run-ci.ts`).
Details: README, "Running in CI".
## Substrate
`mem:common/test-setup`, `mem:common/component-data-model`, `mem:common/component-swap-pipeline`,
`mem:frontend/testing`.

View File

@ -23,7 +23,7 @@ From `frontend/`:
- JS lint currently no-ops via `pnpm run lint:js`.
- SCSS lint: `pnpm run lint:scss`.
- Format checks: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`, `pnpm run check-fmt:scss`.
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`. After running `fmt:*`, `check-fmt:*` is redundant.
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`.
- Translation formatting after i18n edits: `pnpm run translations`.
**Before linting:** if delimiter errors are suspected (after LLM edits, or

View File

@ -1,100 +0,0 @@
# Media Processor
Stateless HTTP service for Penpot image and font processing. Handles image info extraction, thumbnail generation (sharp), and font conversion (FontForge, woff-tools).
## Tech Stack
- Language: TypeScript
- Runtime: Node.js
- Framework: Express
- Image processing: sharp (libvips)
- Font processing: FontForge (TTF/OTF), sfnt2woff, woff2_decompress
- Upload handling: multer (hybrid storage: memory for small, disk for large)
- Logging: pino (with optional Loki transport)
- Config validation: Zod
- Testing: Vitest
- Package Manager: pnpm
## Project Structure
```
media-processor/
├── src/
│ ├── index.ts # Express app setup, routes, middleware
│ ├── config.ts # Zod-validated env config, HKDF key derivation
│ ├── types.ts # TypeScript type definitions
│ ├── upload.ts # Multer configuration, getFileBuffer helper
│ ├── upload-storage.ts # Hybrid storage engine (memory < threshold, disk >= threshold)
│ ├── logger.ts # Pino logger setup
│ ├── middleware/
│ │ ├── auth.ts # Timing-safe shared key authentication
│ │ ├── error-handler.ts # ProcessingError class, centralized error handling
│ │ └── timeout.ts # Request timeout middleware
│ ├── routes/
│ │ ├── health.ts # GET /api/health
│ │ ├── image.ts # POST /api/image/info, /api/image/thumbnail
│ │ └── font.ts # POST /api/font/convert
│ └── services/
│ ├── image.ts # sharp-based image info/thumbnail generation
│ ├── font.ts # FontForge/woff-tools font conversion
│ └── errors.ts # throwValidation, throwRestriction, throwProcessing
├── test/ # Vitest test files
├── vitest.config.ts # Test configuration
├── tsconfig.json # TypeScript configuration
├── esbuild.config.mjs # Build configuration
└── package.json # Dependencies and scripts
```
## Key Conventions
### Auth
- Requests authenticated via `x-shared-key` header using timing-safe comparison
- When no key configured, all requests rejected with 403
- Key derived from `PENPOT_SECRET_KEY` via HKDF (blake2b512) or set directly via `PENPOT_MEDIA_PROCESSOR_SHARED_KEY`
### Resource Limits
- Image: max pixels, max width/height enforced before processing
- Font: prlimit wraps FontForge processes with memory (AS) and CPU time limits
- Concurrency: p-queue limits concurrent requests (default 10)
- Upload: hybrid storage — memory for files < 10MB, disk for larger; configurable via `PENPOT_MEDIA_PROCESSOR_MEMORY_THRESHOLD`
- Max file size: configurable (default 350MB)
### Error Handling
- `throwValidation(code, hint)` — 400 errors for invalid input
- `throwRestriction(code, hint)` — 413 errors for resource limits exceeded
- `throwProcessing(code, hint)` — 503 errors for processing failures (e.g., resource limit kills)
### Image Processing
- EXIF orientation applied before dimension validation and thumbnail generation
- sharp caching disabled to prevent unbounded memory growth
- `withoutEnlargement: true` prevents upscaling small images
### Font Conversion
- Supported formats: TTF, OTF, WOFF, WOFF2
- SFNT type detected via magic bytes (0x4f54544f = OTF, 0x00010000 = TTF)
- Temp files cleaned up in finally blocks (best-effort)
## Commands
All commands run from `media-processor/` directory:
- `pnpm run test` — Run Vitest test suite
- `pnpm run types:check` — TypeScript type checking (tsc --noEmit)
- `pnpm run fmt` — Format code with Prettier
- `pnpm run fmt:check` — Check formatting without modifying
- `pnpm run build` — Build for production (esbuild)
- `pnpm run start:dev` — Start development server (tsx)
## Docker
- Exposed port: 6065 (configurable via `PENPOT_MEDIA_PROCESSOR_PORT`)
- Must be deployed on internal Docker network only (not public-facing)
- Backend communicates via `PENPOT_MEDIA_PROCESSING_SERVICE_URI`
## Testing Principles
Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Run `pnpm run test` after changes
- Run `pnpm run types:check` after TypeScript changes
- Run `pnpm run fmt:check` before commits

View File

@ -6,7 +6,7 @@ Backend (`app.config`, `PENPOT_*` env vars) is parameterized; deployments choose
- **PostgreSQL**: durable store. Profiles, teams, files, sessions, audit, `storage_object` metadata, the `task` queue, `scheduled_task` cron registry, migrations. File-data also lives here when the file-data backend is `legacy-db`/`db`. One shared DB across all backends.
- **Redis (Valkey-compatible)**: per-backend message bus and cache. Concrete uses: msgbus Pub/Sub for collaborative-editing broadcasts and team/profile-org notifications fired by RPC handlers (`app.rpc.notifications`, `files_update`, `teams`, `websocket`); file-summary cache gated by `enable-redis-cache`; rate-limit counters; and the dispatcher→runner work hand-off list `penpot.worker.queue:<tenant>:<queue>`. `PENPOT_REDIS_URI`.
- **Object storage**: backends `:s3` and `:fs`. S3 in prod; devenv uses MinIO. Holds uploaded media, file-data when the file-data backend is `storage`, exports. Backend-side details (resolve, dedup, bucket set, object lifecycle, and file-data backends): `mem:backend/storage`.
- **Object storage**: backends `:s3` and `:fs`. S3 in prod; devenv uses MinIO. Holds uploaded media, file-data when the file-data backend is `storage`, exports. Backend-side details (resolve, dedup, bucket set, file-data backends): `mem:backend/http-storage-filedata-subtleties`.
- **SMTP mailer**: invitations, password resets, email verification (sent via the `:sendmail` worker task).
- **LDAP** (optional auth provider): helpers in `app.auth.*`, gated by `enable-login-with-ldap`.
@ -30,4 +30,4 @@ Penpot in production lives with both: horizontal-scale deployments accept "exact
## See also
- Devenv composition and the ws0-only worker placement: `mem:devenv/core`.
- Storage backend resolution, dedup, bucket behavior, object lifecycle, and file-data lifecycle: `mem:backend/storage`.
- Storage backend resolution, dedup, file-data lifecycle: `mem:backend/http-storage-filedata-subtleties`.

View File

@ -17,16 +17,9 @@
## Tile/render behavior
- Raster `Fill::Image`: skip `save_layer` unless the shape has an image filter; plain
Rect/Frame (no corners) also skip the container clip (`draw_image_fill` in fills.rs).
- Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring
work; crop-cache rebuild is deferred to the later `Full` so the soft→sharp snap is
compose+present only.
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.
- Plain viewport fast mode (`options.is_viewport_interaction()`) renders from cache and does not flush target output inside `process_animation_frame`; interactive transforms do flush.
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
- Frames with a fill may use `render_frame_container_drop_shadow` (direct rrect +
blur saveLayer on `DropShadows`) when `uses_direct_container_drop_shadow` is true.

View File

@ -9,7 +9,6 @@ repository via GraphQL and REST APIs through the authenticated `gh` CLI.
- Finding issues with no milestone.
- Fetching PR details by number or by milestone.
- Comparing milestone issues against CHANGES.md to find missing entries.
- Listing or inspecting GitHub Security Advisories (GHSA).
## Prerequisites
@ -73,30 +72,6 @@ python3 scripts/gh.py prs --milestone "2.16.0" --state all
**Output**: JSON array to stdout; progress to stderr.
### `advisories`
List or inspect GitHub Security Advisories for the repository.
```bash
# List all advisories (summary view)
python3 scripts/gh.py advisories
# Filter by severity
python3 scripts/gh.py advisories --severity critical
# Filter by state
python3 scripts/gh.py advisories --state triage
# Get full detail for a single advisory
python3 scripts/gh.py advisories GHSA-xvj6-fh9w-gjw7
```
**Summary output fields**: ghsa_id, cve_id, severity, cvss_score, state, summary, cwes, published_at, closed_at, url.
**Detail output** (single advisory) adds: description, vulnerabilities (package, version ranges), credits, timestamps.
**Output**: JSON to stdout; progress to stderr.
## Key principles
- All output is JSON — pipe into `jq` or other tools for further processing.

View File

@ -14,8 +14,6 @@ automatically pull the identity from the local git config `user.name` and `user.
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
Body explaining what changed and why.
Wrap lines at 72 characters — git log and tooling
render long lines poorly. Keep each line concise.
AI-assisted-by: model-name
```
@ -27,7 +25,3 @@ AI-assisted-by: model-name
## Commit Type Emojis
`:bug:` bug fix · `:sparkles:` enhancement · `:tada:` new feature · `:recycle:` refactor · `:lipstick:` cosmetic · `:ambulance:` critical fix · `:books:` docs · `:construction:` WIP · `:boom:` breaking · `:wrench:` config · `:zap:` perf · `:whale:` docker · `:paperclip:` other · `:arrow_up:` dep upgrade · `:arrow_down:` dep downgrade · `:fire:` removal · `:globe_with_meridians:` translations · `:rocket:` epic/highlight
## Referencing Issues
Use `Closes #NNNN` (not `Fixes #NNNN`) to link a commit to a GitHub issue.

View File

@ -42,15 +42,15 @@ PR descriptions follow this structure:
## What
<the problem or feature and its user-facing impact short bullet items where there is more than one point>
<one paragraph: the problem or feature, user-facing impact>
## Why
<root cause or motivation a short paragraph or bullets>
<root cause or motivation, why this change was necessary>
## How
<high-level approach and key decisions bullet items, grouped by area (bold lead-ins) for larger PRs>
<high-level approach, key technical decisions>
```
The "Note:" line is required at the top. Adjust if this is a manual (non-AI) PR.
@ -59,8 +59,6 @@ The "Note:" line is required at the top. Adjust if this is a manual (non-AI) PR.
- **Write for humans.** The diff shows what changed. The description explains why.
- **Be concise.** Focus on reasoning: What was the problem? Why did it happen? How did you solve it?
- **Prefer bullets over paragraphs.** Short bullet items, grouped by area with bold lead-ins where helpful, are far easier to digest than prose; keep any remaining paragraph to a few sentences.
- **No manual line wraps.** Markdown renders adapting to the viewport; hard-wrapped lines degrade rendering. One line per paragraph or bullet, however long.
- **Skip the obvious.** Don't explain what `git diff` already shows.
### What NOT to Include

View File

@ -1,31 +1,26 @@
# the name by which the project can be referenced within Serena/when chatting with the LLM.
# the name by which the project can be referenced within Serena
project_name: "penpot"
# list of languages for which language servers are started (LSP backend only); choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart elixir
# elm erlang fortran fsharp gdscript
# list of languages for which language servers are started; choose from:
# al ansible bash clojure cpp
# cpp_ccls crystal csharp csharp_omnisharp dart
# elixir elm erlang fortran fsharp
# go groovy haskell haxe hlsl
# html java json julia kotlin
# latex lean4 lua luau markdown
# matlab msl nix ocaml pascal
# perl php php_phpactor php_phpantom powershell
# python python_jedi python_pyrefly python_ty r
# rego ruby ruby_solargraph rust scala
# scss solidity svelte swift systemverilog
# terraform toml typescript typescript_vts vue
# yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# java json julia kotlin lean4
# lua luau markdown matlab msl
# nix ocaml pascal perl php
# php_phpactor powershell python python_jedi python_ty
# r rego ruby ruby_solargraph rust
# scala solidity swift systemverilog terraform
# toml typescript typescript_vts vue yaml
# zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
@ -59,19 +54,12 @@ ignore_all_files_in_gitignore: true
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
ls_specific_settings: {}
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
# Example:
# ignored_paths:
# - "examples/**"
# - ".worktrees/**"
# - "**/bin/**"
# - "**/obj/**"
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
@ -142,38 +130,13 @@ ignored_memory_patterns: []
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
added_modes:
# list of additional workspace folder paths for cross-package reference support.
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries, but these folders are not indexed by Serena,
# i.e. the respective symbols will not be found using Serena's symbol search tools.
# symbols and references across package boundaries.
# Currently supported for: TypeScript.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
ls_additional_workspace_folders: []
# list of workspace folder paths (LSP backend only).
# These folders will be used to build up Serena's symbol index.
# Paths must be within the project root and should thus be relative to the project root.
# Furthermore, the paths should not be filtered by ignore settings.
# Default setting: The entire project root folder (".") is considered.
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
# ls_workspace_folders:
# - "./subproject1"
# - "./subproject2"
ls_workspace_folders:
- .
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
# the command runs in the project root directory and is only executed if the project is trusted
# (see trusted_project_path_patterns in the global configuration).
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
# example: activation_command: "npx nx run-many -t build"
activation_command:
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
# must be a positive number.
activation_command_timeout: 180.0
additional_workspace_folders: []

View File

@ -8,9 +8,6 @@
wait for the user to push. Do not change the remote URL, do not switch SSH↔HTTPS.
- **Never amend a commit that has been pushed** unless the user explicitly asks.
If the user pushes, treat that commit as final from the agent's side.
- **Never pipe test output directly to filters** (`| head`, `| tail`, `| grep`, etc.).
Always redirect to a file first: `command > /tmp/output.txt 2>&1`, then read/grep the file.
This prevents hiding test failures. See `mem:testing` for details.
- **Read the workflow memory BEFORE the corresponding action**:
- Before `git commit``mem:workflow/creating-commits` (commit format, AI-assisted-by trailer)
- Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, Issue Type)
@ -34,19 +31,6 @@ Skipping this step is the #1 cause of incorrect or incomplete work.
---
## Auto-triggers
- **Security advisory URL pasted** — When the user pastes a URL matching
`github.com/penpot/penpot/security/advisories/GHSA-*`, extract the GHSA ID
from the URL and run `python3 scripts/gh.py advisories <GHSA-ID>` to fetch
full advisory details before proceeding.
## Writing Rules
Use the `ste` skill when the user explicitly requests STE, `/ste`, or ASD-STE100.
---
# Memory system
Memories are the **primary project guidance** — not docs or readme files.
@ -126,5 +110,4 @@ precision while maintaining a strong focus on maintainability and performance.
- `scripts/check-commit` — Validate commit messages against Penpot's commit guidelines.
- `scripts/check-fmt-clj` — Check Clojure formatting without modifying files.
- `scripts/ci` — CI orchestration script for running lint, tests, and format checks across modules. See `scripts/ci --help`.
- `scripts/gh.py` — Multi-purpose GitHub CLI helper. Subcommands: `issues` (list issues in a milestone), `prs` (fetch PR details), `advisories` (list/inspect security advisories). See `python3 scripts/gh.py --help`.

View File

@ -1,54 +1,15 @@
# CHANGELOG
## 2.18.0 (Unreleased)
## 2.17.1 (Unreleased)
### :bug: Bugs fixed
- Fix MCP integration hanging when the Penpot tab is backgrounded or frozen by the browser [#10323](https://github.com/penpot/penpot/issues/10323) (PR: [#10392](https://github.com/penpot/penpot/pull/10392))
- Fix synced component copy not reflowing children after spacing token update [#9892](https://github.com/penpot/penpot/issues/9892)
- Fix spacebar activating pan mode while typing a comment (by @Krishcode264) [#10285](https://github.com/penpot/penpot/issues/10285) (PR: [#10287](https://github.com/penpot/penpot/pull/10287))
- Fix plugin API rejecting negative letterSpacing values (by @filipsajdak) [#9780](https://github.com/penpot/penpot/issues/9780) (PR: [#10257](https://github.com/penpot/penpot/pull/10257))
- Fix plugin API addTheme calls failing with the signature shown in the high-level overview [#10074](https://github.com/penpot/penpot/issues/10074) (PR: [#10359](https://github.com/penpot/penpot/pull/10359))
- Fix empty text shape not being deleted on editor exit [#10540](https://github.com/penpot/penpot/issues/10540) (PR: [#10541](https://github.com/penpot/penpot/pull/10541))
- Fix broken token pills showing wrong default state when not selected [#10524](https://github.com/penpot/penpot/issues/10524) (PR: [#10535](https://github.com/penpot/penpot/pull/10535))
- Replace hyphens with bullets in subscription benefits list [#10547](https://github.com/penpot/penpot/issues/10547) (PR: [#10523](https://github.com/penpot/penpot/pull/10523))
- Fix Chinese (zh-CN) translation showing wrong label for Intersection in board path menu (by @sawirricardo) [#10346](https://github.com/penpot/penpot/issues/10346) (PR: [#10381](https://github.com/penpot/penpot/pull/10381))
### :sparkles: New features & Enhancements
- Group toolbar drawing tools into shape and free-draw flyouts [#9316](https://github.com/penpot/penpot/issues/9316) (PR: [#9480](https://github.com/penpot/penpot/pull/9480), [#10354](https://github.com/penpot/penpot/pull/10354))
- Add outline stroke to Paths [#9961](https://github.com/penpot/penpot/issues/9961) (PR: [#8677](https://github.com/penpot/penpot/pull/8677))
- Make throwValidationErrors default to true for v2 manifest plugins [#10401](https://github.com/penpot/penpot/issues/10401) (PR: [#10433](https://github.com/penpot/penpot/pull/10433))
- Add dedicated Line and Arrow drawing tools (by @davidv399) [#9145](https://github.com/penpot/penpot/issues/9145) (PR: [#9146](https://github.com/penpot/penpot/pull/9146))
- Refactor wasm rulers and UI state [#10116](https://github.com/penpot/penpot/issues/10116) (PR: [#10461](https://github.com/penpot/penpot/pull/10461))
- Improve team invitations modal in the dashboard [#10484](https://github.com/penpot/penpot/issues/10484) (PR: [#10459](https://github.com/penpot/penpot/pull/10459))
## 2.17.1
### :bug: Bugs fixed
- Fix overrides lost after switching component variant [#10588](https://github.com/penpot/penpot/issues/10588) (PR: [#10619](https://github.com/penpot/penpot/pull/10619))
- Fix malformed get-font-variants request when team-id is missing from dashboard URL [#10644](https://github.com/penpot/penpot/issues/10644) (PR: [#10645](https://github.com/penpot/penpot/pull/10645))
- Fix malformed get-profiles-for-file-comments request when file-id is missing from workspace URL [#10652](https://github.com/penpot/penpot/issues/10652) (PR: [#10655](https://github.com/penpot/penpot/pull/10655))
- Fix internal error when dragging inner layout with Boolean operations [#10647](https://github.com/penpot/penpot/issues/10647) (PR: [#10778](https://github.com/penpot/penpot/pull/10778))
- Fix frontend throwing raw TypeError on undefined .getData receivers across import, paste, drag, and text editor paths [#10709](https://github.com/penpot/penpot/issues/10709) (PR: [#10718](https://github.com/penpot/penpot/pull/10718))
- Fix workspace crash with 'can't access dead object' in Firefox when navigating between pages [#10719](https://github.com/penpot/penpot/issues/10719) (PR: [#10721](https://github.com/penpot/penpot/pull/10721))
- Fix workspace crash when holding an arrow key on a selection due to excessive re-renders [#10726](https://github.com/penpot/penpot/issues/10726) (PR: [#10736](https://github.com/penpot/penpot/pull/10736))
- Fix dashboard sidebar throwing removeChild NotFoundError during rapid keyboard navigation [#10714](https://github.com/penpot/penpot/issues/10714) (PR: [#10715](https://github.com/penpot/penpot/pull/10715))
- Fix asset download failing with S3 auth conflict when using access token [#10776](https://github.com/penpot/penpot/issues/10776) (PR: [#10777](https://github.com/penpot/penpot/pull/10777))
- Fix import worker crashing when importing non-Penpot zip files [#10781](https://github.com/penpot/penpot/issues/10781) (PR: [#10782](https://github.com/penpot/penpot/pull/10782))
- Fix internal error when dragging inner layout with Boolean operations [#10647](https://github.com/penpot/penpot/issues/10647) (PR: [#10778](https://github.com/penpot/penpot/pull/10778))
- Fix viewer crash with WASM panic when opening URL with page-id [#10800](https://github.com/penpot/penpot/issues/10800) (PR: [#10805](https://github.com/penpot/penpot/pull/10805))
- Fix backend returning 500 when JSON request body has unrecognized escape sequence [#10804](https://github.com/penpot/penpot/issues/10804) (PR: [#10808](https://github.com/penpot/penpot/pull/10808))
- Fix color picker eyedropper crashing when viewport is unmounted during pointer move [#10811](https://github.com/penpot/penpot/issues/10811) (PR: [#10812](https://github.com/penpot/penpot/pull/10812))
- Fix flex layout crash when dragging shapes with missing bounds [#10843](https://github.com/penpot/penpot/issues/10843) (PR: [#10845](https://github.com/penpot/penpot/pull/10845))
- Fix export failing when shape has blank layer name [#10849](https://github.com/penpot/penpot/issues/10849) (PR: [#10852](https://github.com/penpot/penpot/pull/10852))
- Fix area selection (marquee) being aborted by select-shapes interrupt [#10872](https://github.com/penpot/penpot/issues/10872) (PR: [#10870](https://github.com/penpot/penpot/pull/10870))
- Fix gradient editor sending invalid stop offset when clicking outside gradient line [#10879](https://github.com/penpot/penpot/issues/10879) (PR: [#10881](https://github.com/penpot/penpot/pull/10881))
- Fix audit event validation failing when error reports contain string profile-id and missing token context [#10897](https://github.com/penpot/penpot/issues/10897) (PR: [#10898](https://github.com/penpot/penpot/pull/10898))
- Fix MCP tool call timeout being too low for some operations [#10953](https://github.com/penpot/penpot/issues/10953) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
- Fix MCP requests running into timeouts after leaving a file in Penpot [#10958](https://github.com/penpot/penpot/issues/10958) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
- Fix duplicate WebSocket MCP connection attempts deregistering the original connection's routing entries [#10961](https://github.com/penpot/penpot/issues/10961) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
## 2.17.0

View File

@ -160,6 +160,6 @@ This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS SUBSIDIARY SL
Copyright (c) KALEIDOS INC Sucursal en España SL
```
Penpot is a Kaleidos [open source project](https://kaleidos.net/)

View File

@ -6,7 +6,7 @@
org.clojure/clojure {:mvn/version "1.12.5"}
org.clojure/tools.namespace {:mvn/version "1.5.1"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-12"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-11"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@ -34,28 +34,27 @@
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc
{:mvn/version "1.3.1118"}
{:mvn/version "1.3.1108"}
metosin/reitit-core {:mvn/version "0.10.1"}
nrepl/nrepl {:mvn/version "1.7.0"}
org.postgresql/postgresql {:mvn/version "42.7.13"}
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.1"}
org.postgresql/postgresql {:mvn/version "42.7.12"}
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.0"}
com.zaxxer/HikariCP {:mvn/version "7.1.0"}
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
io.whitfin/siphash {:mvn/version "2.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
org.passay/passay {:mvn/version "1.6.6"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
org.jsoup/jsoup {:mvn/version "1.23.1"}
org.jsoup/jsoup {:mvn/version "1.22.2"}
at.yawk.lz4/lz4-java
{:mvn/version "1.11.1"}
{:mvn/version "1.11.0"}
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
@ -64,8 +63,8 @@
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.50.1"}
software.amazon.awssdk/sts {:mvn/version "2.50.1"}}
software.amazon.awssdk/s3 {:mvn/version "2.46.18"}
software.amazon.awssdk/sts {:mvn/version "2.46.18"}}
:paths ["src" "resources" "target/classes"]
:aliases

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; This is an example on how it can be executed:
;; clojure -Scp $(cat classpath) -M dev/script-fix-sobjects.clj

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns user
(:require

View File

@ -4,25 +4,23 @@
"license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL",
"private": true,
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
},
"dependencies": {
"eventsource-parser": "^3.0.6",
"luxon": "^3.7.2",
"sax": "^1.6.1"
"luxon": "^3.4.4",
"sax": "^1.6.0"
},
"devDependencies": {
"nodemon": "^3.1.14",
"source-map-support": "^0.5.21",
"ws": "^8.21.1"
"ws": "^8.21.0"
},
"scripts": {
"lint:clj": "clj-kondo --config-dir ../.clj-kondo --lint ../common/src src/",
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"test:e2e": "node --test --test-concurrency=1 test/e2e/*.test.mjs"
"lint": "clj-kondo --parallel --lint ../common/src src/",
"check-fmt": "cljfmt check --parallel=true src/ test/",
"fmt": "cljfmt fix --parallel=true src/ test/"
}
}

41
backend/pnpm-lock.yaml generated
View File

@ -8,15 +8,12 @@ importers:
.:
dependencies:
eventsource-parser:
specifier: ^3.0.6
version: 3.1.0
luxon:
specifier: ^3.7.2
specifier: ^3.4.4
version: 3.7.2
sax:
specifier: ^1.6.1
version: 1.6.1
specifier: ^1.6.0
version: 1.6.0
devDependencies:
nodemon:
specifier: ^3.1.14
@ -25,8 +22,8 @@ importers:
specifier: ^0.5.21
version: 0.5.21
ws:
specifier: ^8.21.1
version: 8.21.1
specifier: ^8.21.0
version: 8.21.0
packages:
@ -42,9 +39,9 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@5.0.9:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
brace-expansion@5.0.7:
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@ -66,10 +63,6 @@ packages:
supports-color:
optional: true
eventsource-parser@3.1.0:
resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
engines: {node: '>=18.0.0'}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@ -137,8 +130,8 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
sax@1.6.1:
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
semver@7.8.5:
@ -172,8 +165,8 @@ packages:
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
ws@8.21.1:
resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@ -195,7 +188,7 @@ snapshots:
binary-extensions@2.3.0: {}
brace-expansion@5.0.9:
brace-expansion@5.0.7:
dependencies:
balanced-match: 4.0.4
@ -223,8 +216,6 @@ snapshots:
optionalDependencies:
supports-color: 5.5.0
eventsource-parser@3.1.0: {}
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@ -256,7 +247,7 @@ snapshots:
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.9
brace-expansion: 5.0.7
ms@2.1.3: {}
@ -283,7 +274,7 @@ snapshots:
dependencies:
picomatch: 2.3.2
sax@1.6.1: {}
sax@1.6.0: {}
semver@7.8.5: {}
@ -310,4 +301,4 @@ snapshots:
undefsafe@2.0.5: {}
ws@8.21.1: {}
ws@8.21.0: {}

View File

@ -1,2 +0,0 @@
minimumReleaseAgeExclude:
- brace-expansion@5.0.8 || 5.0.9

View File

@ -195,45 +195,21 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20"
style="display:inline-block;vertical-align:middle;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20" style="display:inline-block;vertical-align:middle;">
<tr>
<td width="20" height="20" align="center" valign="middle"
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
{% if organization.initials %}{{organization.initials}}{% endif %}
</td>
</tr>
</table>
<span
style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
{{ organization.name|abbreviate:50 }}
<span style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
{{ organization.name|abbreviate:50 }}
</span>
</div>
</td>
</tr>
{% if organization.sso-active %}
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
teams and files now goes through your organization's identity provider.
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you can't get in, your account probably isn't in the directory yet.
To get access, contact the organization owner.
</div>
</td>
</tr>
{% endif %}
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">

View File

@ -0,0 +1,10 @@
Hello!
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:25 }}”.
Accept invitation using this link:
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
The Penpot team.

View File

@ -1,17 +0,0 @@
Hello!
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:50 }}”.
{% if organization.sso-active %}
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files now goes
through your organization's identity provider.
If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
{% endif %}
Accept invitation using this link:
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
The Penpot team.

View File

@ -186,31 +186,10 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:50 }}”{% if
organization %}
part of the organization “{{ organization.name|abbreviate:50 }}”{% endif %}.</div>
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if organization %}
part of the organization “{{ organization|abbreviate:25 }}”{% endif %}.</div>
</td>
</tr>
{% if organization.sso-active %}
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to
its teams and files now goes through your organization's identity provider.
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you can't get in, your account probably isn't in the directory yet.
To get access, contact the organization owner.
</div>
</td>
</tr>
{% endif %}
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">

View File

@ -1,13 +1,6 @@
Hello!
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:50 }}"{% if organization %}, part of the organization "{{ organization.name|abbreviate:50 }}"{% endif %}.
{% if organization.sso-active %}
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files now goes
through your organization's identity provider.
If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
{% endif %}
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}.
Accept invitation using this link:

View File

@ -1,231 +0,0 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-px-425 {
width: 425px !important;
max-width: 425px;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E5E5E5;">
<div style="background-color:#E5E5E5;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Hi,
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
teams and files now goes through your organization's identity provider.
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you can't get in, your account probably isn't in the directory yet. To get access, contact the
organization owner.
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
{% include "app/email/includes/footer.html" %}
</div>
</body>
</html>

View File

@ -1 +0,0 @@
“{{ organization-name|abbreviate:25 }}” uses single sign-on

View File

@ -1,8 +0,0 @@
Hi,
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files now goes
through your organization's identity provider.
If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
The Penpot team.

View File

@ -39,16 +39,4 @@
{:permits 3}
:create-file-snapshot/by-profile
{:permits 1 :queue 2 :timeout 60000}
:send-user-feedback/global
{:permits 4}
:send-user-feedback/by-profile
{:permits 1 :queue 3}
:import-binfile/global
{:permits 4}
:import-binfile/by-profile
{:permits 1 :queue 2}}
{:permits 1 :queue 2 :timeout 60000}}

View File

@ -1,308 +1,11 @@
;; Example rlimit.edn file
^{:refresh "30s"}
{:default
[[:default :window "200000/h"]]
;; ═══════════════════════════════════════════════
;; Auth & Identity — public, unauthenticated
;; ═══════════════════════════════════════════════
#{:main/login-with-password}
[[:auth-password :bucket "100/50/1m"]]
;; #{:main/get-teams}
;; [[:burst :bucket "5/5/5s"]]
#{:main/login-with-ldap}
[[:auth-ldap :bucket "20/10/5m"]]
#{:main/register-profile}
[[:auth-register :bucket "20/10/15m"]]
#{:main/request-profile-recovery
:main/prepare-register-profile}
[[:auth-recovery :bucket "100/50/5m"]]
#{:main/recover-profile
:main/verify-token}
[[:auth-token :bucket "100/50/1m"]]
;; ═══════════════════════════════════════════════
;; SSRF vectors — URL fetch endpoints
;; ═══════════════════════════════════════════════
#{:main/create-file-media-object-from-url}
[[:url-fetch :bucket "100/50/5m"]]
#{:main/create-webhook
:main/update-webhook}
[[:webhook-validation :bucket "20/10/5m"]]
;; ═══════════════════════════════════════════════
;; Search — full sequential scan risk
;; ═══════════════════════════════════════════════
#{:main/search-files}
[[:search :bucket "60/30/1m"]]
;; ═══════════════════════════════════════════════
;; Feedback & Invitations — email-sending
;; ═══════════════════════════════════════════════
#{:main/send-user-feedback
:main/create-team-invitations}
[[:email-send :bucket "30/15/5m"]]
;; ═══════════════════════════════════════════════
;; Media & File heavy ops
;; ═══════════════════════════════════════════════
#{:main/upload-file-media-object}
[[:image-upload :bucket "200/100/1m"]]
#{:main/create-file-object-thumbnail
:main/delete-file-object-thumbnails
:main/get-file-object-thumbnails}
[[:thumbnail-ops :bucket "5000/3000/1m"]]
#{:main/get-file-data-for-thumbnail
:main/create-file-thumbnail}
[[:thumbnail-data :bucket "100/50/1m"]]
;; ═══════════════════════════════════════════════
;; UI navigation reads — high frequency
;; ═══════════════════════════════════════════════
#{:main/get-teams}
[[:get-teams :bucket "5000/2500/30s"]]
#{:main/get-team-members}
[[:get-team-members :bucket "4000/2000/30s"]]
#{:main/get-profile}
[[:get-profile :bucket "500/250/30s"]]
#{:main/get-font-variants}
[[:get-font-variants :bucket "250/125/30s"]]
#{:main/get-comment-threads}
[[:get-comment-threads :bucket "500/250/30s"]]
#{:main/get-profiles-for-file-comments}
[[:get-profiles-for-file-comments :bucket "300/150/30s"]]
#{:main/get-file-libraries}
[[:get-file-libraries :bucket "200/100/30s"]]
#{:main/get-projects}
[[:get-projects :bucket "120/60/30s"]]
#{:main/get-team-recent-files
:main/get-unread-comment-threads}
[[:get-team-recent :bucket "120/60/30s"]]
#{:main/get-page}
[[:get-page :bucket "150/75/30s"]]
#{:main/get-access-tokens
:main/get-subscription-usage}
[[:get-access-tokens :bucket "150/75/30s"]]
#{:main/get-enabled-flags}
[[:get-enabled-flags :bucket "250/125/30s"]]
#{:main/get-builtin-templates}
[[:get-builtin-templates :bucket "200/100/30s"]]
#{:main/get-project
:main/get-project-files}
[[:get-project-info :bucket "80/40/30s"]]
#{:main/get-file}
[[:get-file :bucket "180/90/1m"]]
#{:main/get-team-shared-files
:main/get-team-info
:main/get-team-users
:main/get-team-invitations
:main/get-team-deleted-files
:main/get-sso-provider}
[[:get-team-info :bucket "60/30/30s"]]
#{:main/get-comments
:main/get-file-snapshots
:main/get-library-usage
:main/has-file-libraries}
[[:get-misc-list :bucket "300/150/30s"]]
#{:main/get-comment-thread
:main/get-library-file-references}
[[:get-misc-single :bucket "60/30/30s"]]
#{:main/get-file-info
:main/get-view-only-bundle
:main/get-all-projects
:main/get-owned-teams
:main/get-team-stats
:main/get-file-summary
:main/get-file-stats
:main/get-file-fragment}
[[:get-light :bucket "60/30/30s"]]
;; ═══════════════════════════════════════════════
;; File mutations — editing active
;; ═══════════════════════════════════════════════
#{:main/update-file}
[[:update-file :bucket "1000/500/1m"]]
#{:main/create-file
:main/rename-file
:main/duplicate-file
:main/move-files}
[[:file-create :bucket "60/30/1m"]]
#{:main/delete-file}
[[:file-delete :bucket "80/40/1m"]]
#{:main/set-file-shared
:main/update-file-library-sync-status
:main/ignore-file-library-sync-status
:main/link-file-to-library
:main/unlink-file-from-library
:main/create-file-snapshot
:main/restore-file-snapshot
:main/update-file-snapshot
:main/delete-file-snapshot
:main/lock-file-snapshot
:main/unlock-file-snapshot}
[[:file-mutations :bucket "80/40/1m"]]
;; ═══════════════════════════════════════════════
;; Project mutations
;; ═══════════════════════════════════════════════
#{:main/create-project}
[[:project-create :bucket "100/50/1m"]]
#{:main/delete-project
:main/rename-project
:main/duplicate-project
:main/move-project
:main/update-project-pin}
[[:project-mutations :bucket "40/20/1m"]]
;; ═══════════════════════════════════════════════
;; Team mutations
;; ═══════════════════════════════════════════════
#{:main/create-team
:main/update-team
:main/delete-team
:main/update-team-photo
:main/update-team-member-role
:main/delete-team-member
:main/leave-team
:main/create-team-with-invitations
:main/create-team-access-request
:main/permanently-delete-team-files
:main/restore-deleted-team-files}
[[:team-mutations :bucket "60/30/1m"]]
;; ═══════════════════════════════════════════════
;; Comment operations
;; ═══════════════════════════════════════════════
#{:main/create-comment-thread
:main/create-comment
:main/update-comment
:main/delete-comment
:main/mark-all-threads-as-read}
[[:comment-basic :bucket "30/15/1m"]]
#{:main/update-comment-thread
:main/update-comment-thread-status
:main/update-comment-thread-position
:main/update-comment-thread-frame
:main/delete-comment-thread}
[[:comment-thread :bucket "80/40/1m"]]
;; ═══════════════════════════════════════════════
;; Profile operations
;; ═══════════════════════════════════════════════
#{:main/update-profile
:main/update-profile-props
:main/update-profile-photo
:main/update-profile-password
:main/update-profile-notifications
:main/delete-profile
:main/delete-profile-photo
:main/request-email-change}
[[:profile-mutations :bucket "30/15/1m"]]
;; ═══════════════════════════════════════════════
;; Font operations
;; ═══════════════════════════════════════════════
#{:main/create-font-variant
:main/delete-font
:main/delete-font-variant
:main/update-font
:main/download-font
:main/download-font-family}
[[:font-ops :bucket "100/50/1m"]]
;; ═══════════════════════════════════════════════
;; Access tokens
;; ═══════════════════════════════════════════════
#{:main/create-access-token
:main/delete-access-token}
[[:access-token :bucket "60/30/1m"]]
;; ═══════════════════════════════════════════════
;; Export / Import
;; ═══════════════════════════════════════════════
#{:main/export-binfile
:main/import-binfile
:main/clone-template}
[[:export-import :bucket "80/40/1m"]]
;; ═══════════════════════════════════════════════
;; Upload sessions
;; ═══════════════════════════════════════════════
#{:main/create-upload-session
:main/upload-chunk
:main/assemble-file-media-object}
[[:upload-session :bucket "100/50/1m"]]
;; ═══════════════════════════════════════════════
;; Webhooks
;; ═══════════════════════════════════════════════
#{:main/get-webhooks
:main/delete-webhook}
[[:webhook-read :bucket "20/10/1m"]]
;; ═══════════════════════════════════════════════
;; Share links
;; ═══════════════════════════════════════════════
#{:main/create-share-link
:main/delete-share-link}
[[:share-link :bucket "10/5/1m"]]
;; ═══════════════════════════════════════════════
;; Organization operations
;; ═══════════════════════════════════════════════
#{:main/add-team-to-organization
:main/remove-team-from-org
:main/all-org-members-in-team
:main/all-team-members-in-orgs
:main/get-owned-organizations-summary
:main/get-leave-org-summary
:main/leave-org
:main/check-org-members
:main/get-team-invitation-token
:main/delete-team-invitation
:main/check-team-external-invitations}
[[:org-ops :bucket "20/10/1m"]]
;; ═══════════════════════════════════════════════
;; Audit & stats
;; ═══════════════════════════════════════════════
#{:main/push-audit-events}
[[:audit-events :bucket "1000/500/1m"]]
#{:main/logout
:main/get-error-report
:main/get-error-reports
:main/get-current-mcp-token
:main/get-nitrate-connectivity
:main/check-nitrate-sso
:main/redeem-nitrate-activation-code
:main/create-demo-profile
:main/get-subscription-warning}
[[:misc-light :bucket "100/50/1m"]]}
;; #{:main/get-profile}
;; [[:burst :bucket "60/60/1m"]]
}

View File

@ -1,10 +1,9 @@
#!/usr/bin/env bash
export PENPOT_ADMIN_CONSOLE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
export PENPOT_NEXUS_SHARED_KEY=super-secret-nexus-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
export PENPOT_MEDIA_PROCESSOR_SHARED_KEY=super-secret-media-processor-key
# DEPRECATED: only used for subscriptions
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
@ -13,10 +12,6 @@ export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
# PENPOT_DATABASE_*, PENPOT_REDIS_URI, PENPOT_OBJECTS_STORAGE_*, AWS_*) is owned by
# docker/devenv/defaults.env and injected via the main service's env block.
if [ -f /home/selfsigned.crt ]; then
export NODE_EXTRA_CA_CERTS=/home/selfsigned.crt;
fi
# Background worker flag is per-instance. Defaults to enabled (ws0); ws1+
# overlays set PENPOT_BACKEND_WORKER=false so scheduled and async tasks only
# run on ws0, keeping notification Pub/Sub bound to a single Valkey. See
@ -26,8 +21,6 @@ if [[ "${PENPOT_BACKEND_WORKER:-true}" == "true" ]]; then
__worker_flag="enable-backend-worker"
fi
export PENPOT_MEDIA_PROCESSING_SERVICE_URI=http://localhost:6065
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-login-with-password \
@ -43,7 +36,6 @@ export PENPOT_FLAGS="\
enable-feature-fdata-objects-map \
enable-audit-log \
enable-transit-readable-response \
disable-remote-media-processing \
enable-demo-users \
enable-user-feedback \
disable-secure-session-cookies \
@ -79,7 +71,7 @@ export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800
export PENPOT_USER_FEEDBACK_DESTINATION="support@example.com"
export PENPOT_ADMIN_CONSOLE_URI=http://localhost:3000/admin-console
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000/admin-console
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
@ -105,3 +97,5 @@ function setup_minio() {
mc alias set penpot-s3/ "${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}" minioadmin minioadmin -q
mc mb "penpot-s3/${PENPOT_OBJECTS_STORAGE_S3_BUCKET}" -p -q
}

View File

@ -4,7 +4,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) KALEIDOS SUBSIDIARY SL
# Copyright (c) KALEIDOS INC Sucursal en España SL
import argparse
import json

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.auth
(:require

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.auth.ldap
(:require

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.auth.oidc
"OIDC client implementation."
@ -459,10 +459,9 @@
(let [{:keys [status body]} (http/req cfg req {:skip-ssrf-check? (:skip-ssrf-check? provider)})]
(if (= status 200)
(let [data (json/decode body)
data {:token/access (get data :access_token)
:token/id (get data :id_token)
:token/type (get data :token_type)
:token/expires-in (get data :expires_in)}]
data {:token/access (get data :access_token)
:token/id (get data :id_token)
:token/type (get data :token_type)}]
(l/trc :hint "access token fetched"
:token-id (:token/id data)
:token-type (:token/type data)
@ -647,15 +646,6 @@
(assoc :query (u/map->query-string params)))]
(redirect-response uri))))
(defn- redirect-with-organization-sso-error
[{:keys [dest-url organization-id organization-name]}]
(-> (str (or dest-url (cf/get :public-uri)))
(u/append-query-param :sso-error true)
(u/append-query-param :organization-id organization-id)
(cond-> organization-name
(u/append-query-param :organization-name organization-name))
(redirect-response)))
(defn- redirect-to-register
[cfg info provider]
(let [info (assoc info
@ -771,186 +761,20 @@
;; ORG SSO HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- organization-sso-oauth-failure-reason
[error]
(case (d/name error)
"access_denied" "access-denied"
("temporarily_unavailable" "server_error") "provider-unavailable"
("invalid_request" "unauthorized_client" "invalid_scope") "invalid-configuration"
"provider-error"))
(defn- organization-sso-exception-failure-reason
[cause]
(let [data (ex-data cause)
status (or (:response-status data)
(:response-status-code data)
(:http-status data))
network-error?
(loop [current cause]
(cond
(nil? current)
false
(or (instance? java.net.ConnectException current)
(instance? java.net.UnknownHostException current)
(instance? java.net.http.HttpTimeoutException current)
(instance? javax.net.ssl.SSLException current))
true
(identical? current (ex-cause current))
false
:else
(recur (ex-cause current))))]
(if (or network-error?
(and (number? status) (<= 500 status 599)))
"provider-unavailable"
(case (:code data)
:unable-to-fetch-access-token "token-exchange-failed"
:unable-to-retrieve-user-info "user-info-failed"
:incomplete-user-info "incomplete-user-info"
:invalid-sso-config "invalid-configuration"
:unable-to-fetch-sso-jwks "provider-unavailable"
:unable-to-auth "access-denied"
"unexpected-error"))))
(defn- submit-organization-sso-auth-event
[cfg request profile-id organization-id name & {:keys [failure-reason]}]
(audit/submit cfg {:type "action"
:name name
:profile-id profile-id
:ip-addr (inet/parse-request request)
:props (d/without-nils
{:organization-id organization-id
:failure-reason failure-reason})
:context (audit/prepare-context-from-request request)}))
(defn submit-organization-sso-auth-started-event
[cfg request profile-id organization-id]
(submit-organization-sso-auth-event
cfg request profile-id organization-id "organization-sso-auth-started"))
(defn submit-organization-sso-auth-failed-event
[cfg request profile-id organization-id cause]
(submit-organization-sso-auth-event
cfg request profile-id organization-id "organization-sso-auth-failed"
:failure-reason (organization-sso-exception-failure-reason cause)))
(defn- submit-organization-sso-oauth-failed-event
[cfg request state-token error]
(try
(let [state (tokens/verify cfg {:token state-token :iss "oidc"})]
(when (:dest-url state)
(submit-organization-sso-auth-event
cfg request (some-> (session/get-session request) :profile-id)
(:organization-id state) "organization-sso-auth-failed"
:failure-reason (organization-sso-oauth-failure-reason error))))
(catch Exception _ nil)))
(defn- non-blank-uri
[value]
(when-not (str/blank? value) value))
(defn organization-sso-discovery-uri
"Return the OIDC discovery URI from an organization SSO config."
[sso]
(non-blank-uri (:issuer sso)))
(defn prepare-organization-sso-provider
"Build an OIDC provider map dynamically from the Nitrate organization SSO config.
Uses OIDC discovery via :issuer when token/auth/user URIs are absent."
[cfg {:keys [client-id client-secret issuer]}]
(defn prepare-org-sso-provider
"Build an OIDC provider map dynamically from the Nitrate org SSO config.
Uses OIDC discovery via :base-url (or :issuer as fallback) when
token/auth/user URIs are absent."
[cfg {:keys [client-id client-secret base-url issuer scopes]}]
(prepare-oidc-provider cfg
{:type "oidc"
:client-id client-id
:client-secret client-secret
:base-uri (some-> (non-blank-uri issuer)
:base-uri (some-> (or base-url issuer)
(str/rtrim "/")
(str "/"))
:scopes default-oidc-scopes}))
(defn build-organization-sso-auth-redirect-uri
"Build the OIDC authorization redirect URI for an organization SSO config.
Raises if the config is incomplete or OIDC discovery fails."
[cfg sso & {:keys [dest-url organization-id provider]}]
(let [organization-id (or organization-id (:organization-id sso))
issuer (organization-sso-discovery-uri sso)
dest-url (or dest-url (str (cf/get :public-uri)))]
(when-not issuer
(ex/raise :type :validation
:code :invalid-sso-config
:hint "missing issuer"))
(let [oidc-provider (or provider (prepare-organization-sso-provider cfg sso))
state-token (tokens/generate cfg {:iss "oidc"
:dest-url dest-url
:organization-id organization-id
:issuer issuer
:exp (ct/in-future "4h")})]
(build-auth-redirect-uri oidc-provider state-token))))
(def ^:private probe-auth-code "penpot-sso-config-probe")
(defn- decode-token-error-response
[body]
(when (and (string? body) (pos? (count body)))
(try
(json/decode body)
(catch Throwable _ nil))))
(defn- token-endpoint-error
[response]
(some-> response :body decode-token-error-response :error d/name))
(defn- token-endpoint-error-description
[response]
(some-> response :body decode-token-error-response :error-description))
(defn- token-endpoint-valid-client-error?
"Token endpoint rejected the dummy auth code but accepted the client credentials."
[response]
(= "invalid_grant" (token-endpoint-error response)))
(defn- token-endpoint-invalid-client-error?
"Token endpoint rejected the client credentials."
[{:keys [status] :as response}]
(let [error (token-endpoint-error response)
description (str/lower (or (token-endpoint-error-description response) ""))]
(or (= status 401)
(#{"invalid_client" "unauthorized_client"} error)
(and (= error "access_denied")
(str/includes? description "unauthorized")))))
(defn- probe-organization-sso-client-credentials
"Probe the token endpoint with a dummy authorization code.
Valid client credentials are expected to answer with `invalid_grant`."
[cfg provider]
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
:code probe-auth-code
:grant_type "authorization_code"
:redirect_uri (build-redirect-uri)}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"
"accept" "application/json"}
:uri (:token-uri provider)
:body (u/map->query-string params)}
response (http/req cfg req {:skip-ssrf-check? (:skip-ssrf-check? provider)})]
(cond
(token-endpoint-valid-client-error? response) true
(token-endpoint-invalid-client-error? response) false
:else false)))
(defn is-organization-sso-config-valid?
"Return true when the SSO config can be discovered, can build a login URL,
and the client credentials are accepted by the token endpoint."
[cfg sso]
(try
(if (organization-sso-discovery-uri sso)
(let [provider (prepare-organization-sso-provider cfg sso)]
(and (build-organization-sso-auth-redirect-uri cfg sso :provider provider)
(probe-organization-sso-client-credentials cfg provider)))
false)
(catch Throwable _ false)))
:scopes (into default-oidc-scopes (or scopes #{}))
:skip-ssrf-check? true}))
(defn- auth-handler
[cfg {:keys [params] :as request}]
@ -969,62 +793,31 @@
{::yres/status 200
::yres/body {:redirect-uri uri}}))
(defn- organization-sso-callback-handler
"Handle the organization-SSO branch of the OIDC callback: state carries
:dest-url exchange the authorization code with the OIDC provider to
verify authentication actually occurred, then redirect back to dest-url."
[cfg request state code]
(let [dest-url (:dest-url state)]
(try
(let [organization-id (:organization-id state)
sso (nitrate/call cfg :get-organization-sso {:organization-id organization-id})
provider (prepare-organization-sso-provider cfg sso)
_info (get-info cfg provider state code)
session (session/get-session request)
exp (ct/in-future {:minutes 15})]
(when (and session organization-id)
(let [props (-> (or (:props session) {})
(update :sso assoc organization-id exp))]
(session/update-session (::session/manager cfg) (assoc session :props props))))
(submit-organization-sso-auth-event
cfg request (:profile-id session) organization-id "organization-sso-auth-succeeded")
(redirect-response dest-url))
(catch Throwable cause
(let [{:keys [code]} (ex-data cause)]
(binding [l/*context* (errors/request->context request)]
(if (some? code)
(l/warn :hint "organization sso callback failed"
:code code
:message (ex-message cause)
:organization-id (:organization-id state))
(l/err :hint "unexpected error on organization sso callback"
:organization-id (:organization-id state)
:cause cause))))
(submit-organization-sso-auth-failed-event
cfg request (some-> (session/get-session request) :profile-id)
(:organization-id state) cause)
(let [organization-id (:organization-id state)
organization-name (:name (nitrate/call cfg :get-organization-summary {:organization-id organization-id}))]
(redirect-with-organization-sso-error
{:dest-url dest-url
:organization-id organization-id
:organization-name organization-name}))))))
(defn- callback-handler
[cfg {:keys [params] :as request}]
(if-let [error (get params :error)]
(do
(submit-organization-sso-oauth-failed-event cfg request (:state params) error)
(redirect-with-error "unable-to-auth" error))
(redirect-with-error "unable-to-auth" error)
(try
(let [code (get params :code)
state (get params :state)
state (tokens/verify cfg {:token state :iss "oidc"})]
;; Organization SSO flow: state carries :dest-url — exchange the authorization
;; Org SSO flow: state carries :dest-url — exchange the authorization
;; code with the OIDC provider to verify authentication actually occurred.
(if (:dest-url state)
(organization-sso-callback-handler cfg request state code)
(if-let [dest-url (:dest-url state)]
(let [team-id (:team-id state)
organization-id (:organization-id state)
sso (nitrate/call cfg :get-org-sso-by-team {:team-id team-id})
provider (prepare-org-sso-provider cfg sso)
;; verify token or throw error
_info (get-info cfg provider state code)
session (session/get-session request)
exp (ct/in-future {:hours 48})]
(when (and session organization-id)
(let [props (-> (or (:props session) {})
(update :sso assoc organization-id exp))]
(session/update-session (::session/manager cfg) (assoc session :props props))))
(redirect-response dest-url))
(let [provider (resolve-provider cfg state)
info (get-info cfg provider state code)

View File

@ -1,53 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.auth.passwords
"Password strength validation using Passay library."
(:require
[app.common.exceptions :as ex])
(:import
[org.passay CharacterCharacteristicsRule CharacterRule EnglishCharacterData PasswordData]))
(defonce ^:private passay-code->translation-key
{"INSUFFICIENT_LOWERCASE" "errors.weak-password.insufficient-lowercase"
"INSUFFICIENT_UPPERCASE" "errors.weak-password.insufficient-uppercase"
"INSUFFICIENT_DIGIT" "errors.weak-password.insufficient-digits"
"INSUFFICIENT_SPECIAL" "errors.weak-password.insufficient-special"})
(defonce ^:private character-characteristics-rule
(doto (CharacterCharacteristicsRule.)
(.setRules [(CharacterRule. EnglishCharacterData/LowerCase 1)
(CharacterRule. EnglishCharacterData/UpperCase 1)
(CharacterRule. EnglishCharacterData/Digit 1)
(CharacterRule. EnglishCharacterData/Special 1)])
(.setNumberOfCharacteristics 4)))
(defn validate-password
"Validates password strength.
Returns nil if valid, or raises exception if invalid.
Checks:
- Minimum length of 8 characters
- At least 1 lowercase letter
- At least 1 uppercase letter
- At least 1 digit
- At least 1 special character"
[password]
(when (< (count password) 8)
(ex/raise :type :validation
:code :weak-password
:hint "password must be at least 8 characters"
:details ["errors.weak-password.too-short"]))
(let [password-data (PasswordData. password)
char-result (.validate character-characteristics-rule password-data)]
(when-not (.isValid char-result)
(ex/raise :type :validation
:code :weak-password
:hint "password must contain at least 1 lowercase letter, 1 uppercase letter, 1 digit, and 1 special character"
:details (->> (.getDetails char-result)
(mapv #(.getErrorCode %))
(mapv passay-code->translation-key)
(filterv some?))))))

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.cleaner
"A collection of helpers for perform cleaning of artifacts; mainly

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.common
"A binfile related file processing common code, used for different
@ -723,7 +723,6 @@
(-> (select-keys file file-attrs)
(assoc :data nil)
(dissoc :team-id)
(dissoc :metadata)
(dissoc :migrations)))
(defn- file->file-data-params
@ -749,17 +748,9 @@
(fmigr/upsert-migrations! conn file))
(let [file (encode-file cfg file)]
(try
(db/insert! conn :file
(file->params file)
(assoc opts ::db/return-keys false))
(catch org.postgresql.util.PSQLException cause
(if (db/duplicate-key-error? cause)
(ex/raise :type :not-found
:code :object-not-found
:hint "file already exists"
:cause cause)
(throw cause))))
(db/insert! conn :file
(file->params file)
(assoc opts ::db/return-keys false))
(->> (file->file-data-params file)
(fdata/upsert! cfg))

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.migrations
"A binfile related migrations handling"

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.v1
"A custom, perfromance and efficiency focused binfile format impl"
@ -174,10 +174,6 @@
(assert-mark m :obj)
(let [size (read-long! input)]
(assert (pos? size) "incorrect header size found on reading header")
(when (> size bfc/max-object-size)
(ex/raise :type :validation
:code :max-file-size-reached
:hint (dm/str "unable to import object with size " size " bytes")))
(let [buff (byte-array size)]
(read-bytes! input buff)
(fres/decode buff)))))

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.v2
"A sqlite3 based binary file exportation with support for exportation

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.v3
"A ZIP based binary file exportation"
@ -392,7 +392,7 @@
params {:type "penpot/export-files"
:version 1
:generated-by (str "penpot/" (:full cf/version))
:referer "penpot"
:refer "penpot"
:files (vec (vals files))
:relations rels}]
(write-entry! output "manifest.json" params))))
@ -734,7 +734,7 @@
:plugin-data plugin-data}))
(defn- import-file
[{:keys [::db/conn ::bfc/project-id ::manifest] :as cfg} {file-id :id file-name :name}]
[{:keys [::db/conn ::bfc/project-id] :as cfg} {file-id :id file-name :name}]
(let [file-id' (bfc/lookup-index file-id)
file (read-file cfg file-id)
media (read-file-media cfg file-id)
@ -801,10 +801,8 @@
(assoc :data data)
(assoc :name file-name)
(assoc :project-id project-id)
(assoc :metadata (d/without-nils
{:generated-by (get manifest :generated-by)
:referer (or (get manifest :referer) (get manifest :refer))}))
(dissoc :options))
file (bfc/process-file cfg file)
file (ctf/check-file file)]

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.config
(:refer-clojure :exclude [get])
@ -52,7 +52,7 @@
:redis-uri "redis://redis/0"
:file-data-backend "db"
:file-data-backend "legacy-db"
:objects-storage-backend "fs"
:objects-storage-fs-directory "assets"
@ -119,9 +119,8 @@
[:allowed-origins {:optional true} [::sm/set :string]]
[:exporter-shared-key {:optional true} :string]
[:admin-console-shared-key {:optional true} :string]
[:nitrate-shared-key {:optional true} :string]
[:nexus-shared-key {:optional true} :string]
[:media-processor-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string]
[:telemetry-uri {:optional true} :string]
@ -148,9 +147,6 @@
[:imagemagick-width-limit {:optional true} :string]
[:imagemagick-height-limit {:optional true} :string]
[:media-processing-service-uri {:optional true} ::sm/uri]
[:media-processing-service-timeout {:optional true} ::sm/int]
[:deletion-delay {:optional true} ::ct/duration]
[:file-clean-delay {:optional true} ::ct/duration]
[:telemetry-enabled {:optional true} ::sm/boolean]
@ -268,7 +264,7 @@
[:netty-io-threads {:optional true} ::sm/int]
[:admin-console-uri {:optional true} ::sm/uri]
[:nitrate-backend-uri {:optional true} ::sm/uri]
;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword]

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.db
(:refer-clojure :exclude [get run!])

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.db.sql
(:refer-clojure :exclude [update])

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.email
"Main api for send emails."
@ -419,19 +419,10 @@
:id ::change-email
:schema schema:change-email))
(def ^:private schema:organization-data
[:map
[:name ::sm/text]
[:initials {:optional true} [:maybe :string]]
[:logo {:optional true} [:maybe ::sm/uri]]
[:avatar-bg-url {:optional true} [:maybe ::sm/uri]]
[:sso-active {:optional true} [:maybe ::sm/boolean]]])
(def ^:private schema:invite-to-team
[:map
[:invited-by ::sm/text]
[:team ::sm/text]
[:organization {:optional true} [:maybe schema:organization-data]]
[:token ::sm/text]])
(def invite-to-team
@ -440,28 +431,27 @@
:id ::invite-to-team
:schema schema:invite-to-team))
(def ^:private schema:invite-to-organization
(def ^:private schema:organization-data
[:map
[:name ::sm/text]
[:initials [:maybe :string]]
[:logo [:maybe ::sm/uri]]
[:avatar-bg-url [:maybe ::sm/uri]]])
(def ^:private schema:invite-to-org
[:map
[:invited-by ::sm/text]
[:user-name [:maybe ::sm/text]]
[:token ::sm/text]
[:organization schema:organization-data]])
(def invite-to-organization
"Organization member invitation email."
(def invite-to-org
"Org member invitation email."
(template-factory
:id ::invite-to-organization
:schema schema:invite-to-organization))
:id ::invite-to-org
:schema schema:invite-to-org))
(def ^:private schema:organization-setup-sso
[:map
[:organization-name ::sm/text]])
(def organization-setup-sso
"Email when an organization set up SSO"
(template-factory
:id ::organization-setup-sso
:schema schema:organization-setup-sso))
(def ^:private schema:renewal-notice
[:map

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.email.blacklist
"Email blacklist provider"

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.email.whitelist
"Email whitelist provider"

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.features.fdata
"A `fdata/*` related feature migration helpers"
@ -12,7 +12,6 @@
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.file :as ctf]
[app.common.types.objects-map :as omap]
[app.config :as cf]
[app.db :as db]
@ -160,17 +159,15 @@
:content-type "application/octet-stream"
:file-id file-id
:id id})
metadata (-> (:metadata params)
(assoc :storage-ref-id (:id sobject)))
metadata {:storage-ref-id (:id sobject)}
params (-> params
(assoc :metadata metadata)
(assoc :data nil))]
(upsert-in-database cfg params))
(= backend "db")
(let [metadata (dissoc (:metadata params) :storage-ref-id)
params (assoc params :metadata metadata)]
(upsert-in-database cfg params))
(->> (dissoc params :metadata)
(upsert-in-database cfg))
(= backend "legacy-db")
(cond
@ -216,11 +213,18 @@
[backend]
(or backend (cf/get :file-data-backend)))
(def ^:private schema:metadata
[:map {:title "Metadata"}
[:storage-ref-id {:optional true} ::sm/uuid]])
(def decode-metadata-with-schema
(sm/decoder schema:metadata sm/json-transformer))
(defn decode-metadata
[metadata]
(some-> metadata
(db/decode-json-pgobject)
(ctf/decode-file-metadata)))
(decode-metadata-with-schema)))
(def ^:private schema:update-params
[:map {:closed true}
@ -228,7 +232,7 @@
[:type [:enum "main" "snapshot" "fragment"]]
[:file-id ::sm/uuid]
[:backend {:optional true} [:enum "db" "legacy-db" "storage"]]
[:metadata {:optional true} ctf/schema:file-metadata]
[:metadata {:optional true} [:maybe schema:metadata]]
[:data {:optional true} bytes?]
[:created-at {:optional true} ::ct/inst]
[:modified-at {:optional true} [:maybe ::ct/inst]]

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.features.file-migrations
"Backend specific code for file migrations. Implemented as permanent feature of files."

View File

@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.features.file-snapshots
(:require

Some files were not shown because too many files have changed in this diff Show More