Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .agents/skills/external/ibexa-documentation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---

@mnocon mnocon Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To create the skill I've used https://github.com/agent-ecosystem/skill-validator , here's the result:

~doc-as-composer-package doc-as-composer-package *17 !1 ?1 ❯ 

skill-validator score evaluate --provider claude-cli .agents/skills/external --rescore
  scoring: ibexa-documentation/SKILL.md

Scoring skill: /Users/marek/Desktop/repos/mnocon/documentation-developer/
doc-as-composer-package/.agents/skills/external/ibexa-documentation

SKILL.md Scores
  Clarity:               5/5
  Actionability:         5/5
  Token Efficiency:      4/5
  Scope Discipline:      5/5
  Directive Precision:   4/5
  Novelty:               4/5
  ──────────────────────────────
  Overall:              4.50/5

  "A tightly scoped, highly actionable skill that 
precisely documents the local Ibexa DXP docs layout, lookup methods, 
and package update workflow with concrete commands; 
minor room to trim and a bit more explicit gating on when to fall back to web/code."
  Novel details: The novel, project-specific details a reviewer should verify are: 
the claim that `vendor/ibexa/documentation-developer/` is installed locally 
and mirrors both `doc.ibexa.co/en/latest/` (developer) 
and `doc.ibexa.co/projects/userguide/en/latest/` (user guide); 
the `developer/llms.txt`/`user/llms.txt` table-of-contents convention; 
and the package's non-standard versioning scheme 
(`MAJOR.MINOR.DATE_OF_TAGGING_YYYYMMDD`, e.g. `5.0.20260101`) 
alongwith the specific `composer outdated --direct "ibexa/documentation-*"` 
and `composer update ibexa/documentation-developer --no-scripts` commands.

I've experimented with various rewrites, but I believe the skill has reached diminishing returns already.
As the scoring is done by a LLM, the score and reasoning fluctuates a bit with every run - but it's consistently getting scored above 4.

Once we make this available internally, we'll be able to get feedback from people using it.

name: ibexa-documentation
description: Look up features, concepts, configuration, extension points, and back office workflows in the locally installed Ibexa DXP documentation. Use whenever working on an Ibexa DXP project and you need to know how something in Ibexa works before writing code, configuration, or answers.

Check failure on line 3 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L3

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 3, "column": 126}}}, "severity": "ERROR"}

Check failure on line 3 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L3

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 3, "column": 178}}}, "severity": "ERROR"}

Check failure on line 3 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L3

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 3, "column": 234}}}, "severity": "ERROR"}
---

# Ibexa DXP documentation

Check failure on line 6 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L6

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 6, "column": 3}}}, "severity": "ERROR"}

Check notice on line 6 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L6

[Ibexa.SentenceCapitalizationInHeadings] Use sentence-style capitalization in headings
Raw output
{"message": "[Ibexa.SentenceCapitalizationInHeadings] Use sentence-style capitalization in headings", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 6, "column": 3}}}, "severity": "INFO"}

The official Ibexa DXP documentation is installed as Markdown files in `vendor/ibexa/documentation-developer/`.

Check failure on line 8 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L8

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 8, "column": 14}}}, "severity": "ERROR"}

Check notice on line 8 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L8

[Ibexa.Passive] Try to avoid passive tense, when possible.
Raw output
{"message": "[Ibexa.Passive] Try to avoid passive tense, when possible.", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 8, "column": 38}}}, "severity": "INFO"}
It matches this project's Ibexa DXP release line (e.g. 5.0).

Check failure on line 9 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L9

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 9, "column": 27}}}, "severity": "ERROR"}
Always consult it before web searches or memory: it works offline, matches the installed version, and links directly to the code in `vendor/`.

It contains two documentation sets:

- `developer/` — APIs, configuration, extension points, tutorials. Mirrors https://doc.ibexa.co/en/latest/
- `user/` — back office, editorial and commerce workflows. Mirrors https://doc.ibexa.co/projects/userguide/en/latest/

Use the documentation for what the code cannot tell you:

- Concepts and the content model: content types, fields, locations, versions, languages, and how they relate.
- Release notes and changes between versions: `developer/release_notes/`.
- Update and migration instructions: `developer/update_and_migration/`.
- Project and feature setup, configuration, and best practices: `developer/infrastructure_and_maintenance/`.

For method signatures and contracts, the code in `vendor/` is authoritative.
Use the documentation for the intent, concepts, and configuration around them.
When the documentation and the installed code don't match, the code is right.

## Finding topics

- Read the set's table of contents: `vendor/ibexa/documentation-developer/developer/llms.txt` (or `user/llms.txt`). Page titles are grouped by section, with relative links.

Check notice on line 30 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L30

[Ibexa.Passive] Try to avoid passive tense, when possible.
Raw output
{"message": "[Ibexa.Passive] Try to avoid passive tense, when possible.", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 30, "column": 129}}}, "severity": "INFO"}
- Search the sets directly: `grep -ril "<topic>" vendor/ibexa/documentation-developer/`.

## Conventions

- Links between pages are relative and work offline. Follow them for related topics.
- PHP API references link to the class source in this project's `vendor/` directory. If the target file doesn't exist, that package isn't installed.

## Keep documentation up to date

The package is versioned as `MAJOR_DXP_VERSION.MINOR_DXP_VERSION.DATE_OF_TAGGING_YYYYMMDD` and must be used with the matching Ibexa DXP release line.

Check notice on line 40 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L40

[Ibexa.Passive] Try to avoid passive tense, when possible.
Raw output
{"message": "[Ibexa.Passive] Try to avoid passive tense, when possible.", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 40, "column": 13}}}, "severity": "INFO"}

Check notice on line 40 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L40

[Ibexa.Passive] Try to avoid passive tense, when possible.
Raw output
{"message": "[Ibexa.Passive] Try to avoid passive tense, when possible.", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 40, "column": 101}}}, "severity": "INFO"}

Check failure on line 40 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L40

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 40, "column": 127}}}, "severity": "ERROR"}
For example, `5.0.20260101` is for Ibexa DXP 5.0.x releases.

Check failure on line 41 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L41

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 41, "column": 36}}}, "severity": "ERROR"}
New version is released when the documentation content is updated.

Check notice on line 42 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L42

[Ibexa.Passive] Try to avoid passive tense, when possible.
Raw output
{"message": "[Ibexa.Passive] Try to avoid passive tense, when possible.", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 42, "column": 13}}}, "severity": "INFO"}

Check notice on line 42 in .agents/skills/external/ibexa-documentation/SKILL.md

View workflow job for this annotation

GitHub Actions / vale

[vale] .agents/skills/external/ibexa-documentation/SKILL.md#L42

[Ibexa.Passive] Try to avoid passive tense, when possible.
Raw output
{"message": "[Ibexa.Passive] Try to avoid passive tense, when possible.", "location": {"path": ".agents/skills/external/ibexa-documentation/SKILL.md", "range": {"start": {"line": 42, "column": 56}}}, "severity": "INFO"}

To check whether the documentation contains all the latest updates, run:

``` bash
composer outdated --direct "ibexa/documentation-*"
```

To update it, run:

```bash
composer update ibexa/documentation-developer --no-scripts
```

Always pass `--no-scripts`: the package contains only Markdown files, so the slow post-update scripts (cache clear, asset and frontend builds) are unnecessary.
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* export-ignore

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file controls what is included in the released package - you can explore the tag content downloaded by Composer to see that the Markdown source files, the HTML PHP API ref etc. are not included.

The result are pure Markdown files, skill file, llms.txt files and composer.json

/.agents export-ignore=false
/developer export-ignore=false
/user export-ignore=false
/composer.json export-ignore=false
/LICENSE export-ignore=false
/README.md export-ignore=false
210 changes: 210 additions & 0 deletions .github/workflows/release_composer_package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
name: "Release Composer package"

on:
schedule:
# Daily at 03:17 UTC. Fans out to every maintained version branch (see determine-branches).
- cron: "17 3 * * *"

@mnocon mnocon Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to run this workflow every day, and create a new tag if there's new content in the documentation.

So it's not tied to the release cycle of the DXP, it's tagged when there's something new in the doc to make sure it's up to date with what's available online.

workflow_dispatch:
inputs:
version:
description: "Version branch to release"
required: true
default: "5.0"
type: string
devdoc_branch:

@mnocon mnocon Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added devdoc_branch and userdoc_branch so that we don't have to add any TMP commits when testing this workflow (as we currently do for the PHP API reference)

description: "Developer documentation branch to check out (defaults to version)"
required: false
default: ""
type: string
userdoc_branch:
description: "User documentation branch to check out (defaults to version)"
required: false
default: ""
type: string

jobs:
determine-branches:
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.set.outputs.branches }}
devdoc_branch: ${{ steps.set.outputs.devdoc_branch }}
userdoc_branch: ${{ steps.set.outputs.userdoc_branch }}
steps:
- id: set
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
# Manual runs release the branch chosen via the "version" input,
# not necessarily the branch the workflow was dispatched from.
branches='["${{ github.event.inputs.version }}"]'
# Left empty when not overridden; the release job then falls back
# to the version branch (matrix.branch) for the checkout ref.
devdoc_branch="${{ github.event.inputs.devdoc_branch }}"
userdoc_branch="${{ github.event.inputs.userdoc_branch }}"
else
# Scheduled runs always execute in the default branch's context, so they must
# explicitly check out each maintained version branch instead of relying on
# GITHUB_REF. Extend this list as new branches become active / drop EOL ones.
branches='["5.0"]'
devdoc_branch=""
userdoc_branch=""
fi
echo "branches=${branches}" >> "$GITHUB_OUTPUT"
echo "devdoc_branch=${devdoc_branch}" >> "$GITHUB_OUTPUT"
echo "userdoc_branch=${userdoc_branch}" >> "$GITHUB_OUTPUT"

release:
needs: determine-branches
runs-on: ubuntu-latest
permissions:
# Needed to commit the generated Markdown and push a tag.
contents: write
strategy:
fail-fast: false
matrix:
branch: ${{ fromJson(needs.determine-branches.outputs.branches) }}

steps:
# Shallow checkout of the branch tip only. The generated Markdown is never
# pushed to the branch itself (see below), only tagged, so the diff target is
# the latest tag — fetched individually in "Find the last tag" instead of
# pulling the full history + every daily tag snapshot (which took minutes).
- uses: actions/checkout@v4
with:
ref: ${{ needs.determine-branches.outputs.devdoc_branch || matrix.branch }}

- name: Check out user documentation
uses: actions/checkout@v4
with:
repository: ibexa/documentation-user
ref: ${{ needs.determine-branches.outputs.userdoc_branch || matrix.branch }}
path: user-docs

- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.13"

- name: Install dependencies
run: |
python -m pip install --upgrade pip

Check warning

Code scanning / SonarCloud

Python dependencies should be locked to verified versions Medium

Using dependencies without locking resolved versions is security-sensitive. See more on SonarQube Cloud
# user-docs pins ibexa-llms-txt from the upstream git repo; drop that line —

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a clash in dependencies - PIP doesn't understand that these:

https://github.com/ibexa/documentation-user/blob/5.0/requirements.txt#L11
https://github.com/ibexa/documentation-developer/blob/5.0/requirements.txt#L11

refer to the same dependency. Hence the grep filtering to make the dependencies resolve

# the editable install from this checkout (requirements.txt: -e .) already
# provides the plugin, and pip refuses two direct references to one package.
grep -v '^ibexa-llms-txt @' user-docs/requirements.txt > user-reqs-filtered.txt
pip install -r requirements.txt -r user-reqs-filtered.txt

Check warning

Code scanning / SonarCloud

Python package manager scripts should not be executed during installation Medium

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here. See more on SonarQube Cloud

Check warning

Code scanning / SonarCloud

Python dependencies should be locked to verified versions Medium

Using dependencies without locking resolved versions is security-sensitive. See more on SonarQube Cloud

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've discussed this with Gemini and Claude - we control ibexa/documentation-user, so it's ok.

Quoting Gemini:

Is it a real risk here?

No, if user-docs/ is entirely controlled by your team. If it’s just your own static internal repo, the risk is negligible (limited to general supply-chain risks on PyPI).


- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 #2.37.2
with:
php-version: "8.3"
coverage: none

# TMP: Uncomment when merging to ibexa repository
# - name: Generate token
# id: generate_token
# uses: actions/create-github-app-token@v2
# with:
# app-id: ${{ secrets.AUTOMATION_CLIENT_ID }}
# private-key: ${{ secrets.AUTOMATION_CLIENT_SECRET }}
# owner: ${{ github.repository_owner }}

- name: Add composer keys for private packagist
run: |
composer config --global http-basic.updates.ibexa.co $SATIS_NETWORK_KEY $SATIS_NETWORK_TOKEN
composer config --global github-oauth.github.com $GITHUB_TOKEN
env:
SATIS_NETWORK_KEY: ${{ secrets.SATIS_NETWORK_KEY }}
SATIS_NETWORK_TOKEN: ${{ secrets.SATIS_NETWORK_TOKEN }}
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} #${{ steps.generate_token.outputs.token }} #TMP: Revert when merging to ibexa repository

# Needed to resolve the PHP API classes referenced by the docs to
# their vendor/ source paths (dump_class_paths.php).
- uses: ramsey/composer-install@a8d0d959dab41457692a5e2041bd9b757a119e3f #v3.2.1
with:
dependency-versions: highest

- name: Build developer documentation
run: mkdocs build --strict
env:
# The cards() macro versions its links from this (as on RTD);
# without it they'd point at en/latest instead of this branch.
READTHEDOCS_VERSION_NAME: ${{ matrix.branch }}

- name: Build user documentation
run: mkdocs build --strict
working-directory: user-docs
env:
READTHEDOCS_VERSION_NAME: ${{ matrix.branch }}

- name: Resolve PHP API classes to vendor paths
run: php tools/llm_package/dump_class_paths.php site user-docs/site class_paths.json

- name: Build package Markdown (developer/, user/)
run: python build_package_docs.py --version "${{ matrix.branch }}"

- name: Copy package README
run: cp tools/llm_package/README.package.md README.md

- name: Smoke-test package output
run: |
composer validate

- name: Find the last tag for this branch
id: last_tag
env:
BRANCH: ${{ matrix.branch }}
run: |
# List tag names on the remote (no object download), then fetch only the
# latest one — the single commit the change check diffs against.
latest_tag=$(git ls-remote --tags origin "refs/tags/${BRANCH}.*" \
| awk '{print $2}' | sed 's|^refs/tags/||' | grep -v '\^{}$' \
| sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1)
if [[ -n "$latest_tag" ]]; then
git fetch --depth=1 origin "refs/tags/${latest_tag}:refs/tags/${latest_tag}"
fi
echo "latest_tag=${latest_tag}" >> "$GITHUB_OUTPUT"

- name: Check for content changes since the last tag
id: changes
env:
LATEST_TAG: ${{ steps.last_tag.outputs.latest_tag }}
run: |
# The generated Markdown is never committed/pushed to the branch itself (only
# tagged), so there's nothing on the branch to diff against — compare the freshly
# built content to what the last tag captured instead. -f because developer/ and
# user/ are gitignored to keep local builds out of the working tree.
git add -f developer user
git add README.md
if [[ -z "$LATEST_TAG" ]]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
# .agents (agent skills, tracked on the branch) is part of the package too;
# include it so skill-only edits also produce a release.
elif git diff --cached --quiet "$LATEST_TAG" -- developer user README.md .agents; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Commit generated Markdown
if: steps.changes.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "Update generated Markdown docs for ${{ matrix.branch }}"

- name: Tag release
if: steps.changes.outputs.changed == 'true'
env:
BRANCH: ${{ matrix.branch }}
run: |
# Push only the tag, never the branch — the branch stays exactly as authored,
# the generated Markdown exists solely as the commit this tag points to.
tag="${BRANCH}.$(date -u +%Y%m%d)"

if git rev-parse "$tag" >/dev/null 2>&1; then
echo "Tag $tag already exists, skipping."
exit 0
fi

git tag "$tag"
git push origin "$tag"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ lychee-report.md
code_samples/_inline_php/
/build/
*.egg-info/
/doc/
/llms.txt
/developer/
/user/
/class_paths.json
/user-docs/
Loading
Loading