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
61 changes: 19 additions & 42 deletions .github/workflows/config-docs-in-sync.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Config Docs In Sync

# The two config parameter pages per server version are generated by
# owncloud/config-to-docs from owncloud/core's config/config.sample.php and
# config/config.apps.sample.php. Everything below the "header end" marker in
# those pages is machine output, and a hand edit there is silently destroyed the
# next time anyone regenerates. This job regenerates and fails on any
# antora-extensions/lib/config-sample-to-adoc.js from owncloud/core's
# config/config.sample.php and config/config.apps.sample.php. Everything below
# the "header end" marker in those pages is machine output, and a hand edit
# there is silently destroyed the next time anyone regenerates. This job regenerates and fails on any
# difference, so the published pages are provably generator output and an edit
# in the wrong place is caught here instead of years later.
#
Expand Down Expand Up @@ -32,48 +32,26 @@ env:
jobs:
config-docs-in-sync:
runs-on: ubuntu-latest
# owncloud-owned image, so no third-party action is needed to provide PHP.
container: owncloudci/php:7.4
# Inside a container the default shell is sh, not bash.
defaults:
run:
shell: bash
concurrency:
group: config-docs-in-sync-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout docs
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Checkout config-to-docs
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
repository: owncloud/config-to-docs
path: .config-to-docs
# Deliberately unpinned. This check asks "do the committed pages match
# what a maintainer running ctd.sh today would produce", so it has to
# track the current generator. The composer deps below are pinned
# because the lock file is that generator's own declared contract.

- name: Install the converter
working-directory: .config-to-docs
# install, not update: the comment parsing lives in
# phpdocumentor/reflection-docblock, which composer.json accepts as
# ^4.3.0 while the lock pins a known-good version. Floating it can
# change the generated output.
run: composer install --no-interaction --no-progress --no-dev --no-scripts
node-version: '22'

- name: Regenerate every version from its core branch
run: |
set -eu

# convert.php exits 0 on both of its own failure paths - unreadable
# input, unwritable output - so nothing here may infer success from an
# exit status. Every file is checked before use, and the pages actually
# regenerated are counted and asserted at the end. Without that, a
# renamed sample in core would leave the pages untouched, the diff
# below would find nothing, and this job would pass having checked
# nothing at all.
# Every file is checked before use, and the pages actually regenerated
# are counted and asserted at the end. Without that, a renamed sample
# in core would leave the pages untouched, the diff below would find
# nothing, and this job would pass having checked nothing at all.
versions=0
generated=0

Expand Down Expand Up @@ -140,13 +118,11 @@ jobs:
fi

# Keep the committed copy to compare against. The comparison
# deliberately does not use git: the pages are just files, and
# inside this container git cannot see the workspace repository.
# deliberately does not use git: the pages are just files.
mkdir -p "$RUNNER_TEMP/committed/$(dirname "$page")"
cp "$page" "$RUNNER_TEMP/committed/$page"

php .config-to-docs/convert.php config:convert-adoc \
--input-file="$sample" --output-file="$page"
node scripts/config-sample-to-adoc.js "$sample" "$page"
generated=$((generated + 1))
done

Expand All @@ -170,7 +146,7 @@ jobs:
run: |
set -eu

# diff, not git diff: see the note in the previous step. Each snapshot
# Plain diff against the snapshots taken above. Each snapshot
# taken before regeneration is compared with the page as it now stands,
# so the diff reads committed -> generated, i.e. what needs applying.
: > "$RUNNER_TEMP/drift.diff"
Expand All @@ -196,7 +172,7 @@ jobs:
{
echo "### Config parameter pages are out of sync"
echo
echo "These pages are generated by [config-to-docs](https://github.com/owncloud/config-to-docs)"
echo "These pages are generated by \`antora-extensions/lib/config-sample-to-adoc.js\`"
echo "from \`config/config.sample.php\` and \`config/config.apps.sample.php\` in"
echo "[owncloud/core](https://github.com/owncloud/core). Everything below the"
echo "\`header end\` marker is machine output."
Expand All @@ -205,15 +181,16 @@ jobs:
echo "* **To change a page's introduction**, edit *above* the marker - that part is kept."
echo "* **If core changed and the page is merely stale**, commit the regenerated page."
echo
echo "To regenerate locally: clone core and config-to-docs, check out the core branch"
echo "matching the version, and run \`./ctd.sh\` in config-to-docs."
echo "To regenerate locally: clone core next to this repo, check out the core branch"
echo "matching the version, and run the \`config-sample-to-adoc\` extension, see"
echo "\`content/server/AGENTS.md\`."
echo
echo '```diff'
cat "$RUNNER_TEMP/drift.diff"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

echo "::error::Committed config parameter pages differ from config-to-docs output - see the job summary for the diff and how to fix it."
echo "::error::Committed config parameter pages differ from the generator output - see the job summary for the diff and how to fix it."
grep -E '^(---|\+\+\+|Only in)' "$RUNNER_TEMP/drift.diff" || true
exit 1

Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ Subdirectory `AGENTS.md` files are loaded on demand when working in that subtree

* `content/AGENTS.md` -- AsciiDoc authoring rules (tables, images, lists, heading capitalisation), xref fixes, `doc.owncloud.com` link conversion, external link maintenance, xref link-text checks
* `content/ocis/AGENTS.md` -- oCIS developer service pages, Antora Collector and Swagger UI extensions
* `content/server/AGENTS.md` -- generated server config parameter pages (`config-sample-to-adoc`) and how to write the sample comments in core
* `ui/AGENTS.md` -- `llms.txt` maintenance and supplemental UI JavaScript

## File Relationship: CLAUDE.md and AGENTS.md

Every `CLAUDE.md` is a symlink to the `AGENTS.md` beside it (root, `content/`, `content/ocis/`, `ui/`). Both names are provided so that different AI tools find their expected filename — Claude Code reads `CLAUDE.md`, OpenAI Codex-based tools read `AGENTS.md`. **Always edit `AGENTS.md` directly**; `CLAUDE.md` reflects all changes automatically via the symlink. When adding a new subdirectory `AGENTS.md`, add the sibling `CLAUDE.md` symlink and list it under "Where the Other Rules Live".
Every `CLAUDE.md` is a symlink to the `AGENTS.md` beside it (root, `content/`, `content/ocis/`, `content/server/`, `ui/`). Both names are provided so that different AI tools find their expected filename — Claude Code reads `CLAUDE.md`, OpenAI Codex-based tools read `AGENTS.md`. **Always edit `AGENTS.md` directly**; `CLAUDE.md` reflects all changes automatically via the symlink. When adding a new subdirectory `AGENTS.md`, add the sibling `CLAUDE.md` symlink and list it under "Where the Other Rules Live".

Keep these files outside the Antora `start_paths` (never inside a `content/<product>/<version>/` directory, which is added and dropped over time) and outside `ui/supplemental/`, otherwise they would be published. `extension-tests/static-files.test.js` guards this.

Expand Down
100 changes: 100 additions & 0 deletions antora-extensions/config-sample-to-adoc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict'

/**
* config-sample-to-adoc — Antora extension
*
* Regenerates the server configuration parameter pages
* (config_sample_php_parameters.adoc, config_apps_sample_php_parameters.adoc)
* from the config.sample.php / config.apps.sample.php files in a local
* owncloud/core checkout. It replaces the retired owncloud/config-to-docs tool.
*
* This is a maintainer tool, not part of the site build: the extension is
* registered with `enabled: false` and is meant to be switched on locally,
* with a core clone on the branch that matches the docs version. It writes
* into content/, so the result is reviewed and committed like any other edit.
*
* Everything above the `// header end do not delete or edit this line` marker
* in a target page is hand-written and kept; everything below it is replaced.
* The target page must already exist and contain the marker.
*
* It listens to playbookBuilt, which fires before content is aggregated, so a
* regenerated page is already picked up by the same build.
*
* antora:
* extensions:
* - require: ./antora-extensions/config-sample-to-adoc.js
* enabled: true
* pairs:
* - source_path: ../core/config/config.sample.php
* target_path: content/server/11.0/modules/admin_manual/pages/configuration/server/config_sample_php_parameters.adoc
* - source_path: ../core/config/config.apps.sample.php
* target_path: content/server/11.0/modules/admin_manual/pages/configuration/server/config_apps_sample_php_parameters.adoc
* report_only: true
* stop_after_processing: true
*
* Config keys:
* pairs (array, required) — Source→target pairs to process.
* source_path (string, required) — Path to a config.sample.php or
* config.apps.sample.php file (a file,
* not a URL). Relative paths are
* resolved from the playbook directory.
* target_path (string, required) — Path to the existing .adoc page in this
* repo. Resolved like source_path.
* report_only (boolean, default: false) — When true, only log which pages would
* change; write nothing.
* stop_after_processing (boolean, default: false) — When true, stop the Antora build with
* exit code 0 once the pages are done.
* Combine with report_only to inspect
* without running a full build.
*/

const fs = require('node:fs')
const path = require('node:path')
const { convert } = require('./lib/config-sample-to-adoc')

module.exports.register = function ({ config }) {
this.once('playbookBuilt', ({ playbook }) => {
// Antora camelCases the keys of extension config, so the snake_case keys of
// the playbook arrive as sourcePath, reportOnly, etc. An unknown key is an
// error rather than ignored: a dry run that silently writes would be bad.
const { pairs, reportOnly = false, stopAfterProcessing = false, ...unknown } = config
if (Object.keys(unknown).length) {
throw new Error(`config-sample-to-adoc: unknown config key(s): ${Object.keys(unknown).join(', ')}`)
}
if (!Array.isArray(pairs) || !pairs.length) {
throw new Error('config-sample-to-adoc: `pairs` is required and must be a non-empty list')
}
const log = (message) => console.log(`[config-sample-to-adoc] ${message}`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The other extensions here (llms-txt, sitemap-cleanup, prerelease-attribute, load-global-site-attributes) log through this.getLogger("config-sample-to-adoc"). Using console.log bypasses --log-level, --log-format json and --log-failure-level, so this output looks different from the rest of the build and can't be silenced.


for (const { sourcePath: source, targetPath: target, ...extra } of pairs) {
if (typeof source !== 'string' || typeof target !== 'string' || Object.keys(extra).length) {
throw new Error('config-sample-to-adoc: every pair needs exactly a `source_path` and a `target_path`')
}
const sourceFile = path.resolve(playbook.dir, source)
const targetFile = path.resolve(playbook.dir, target)
// Unlike the retired PHP tool, a bad path is an error, not a silent no-op.
const sample = readFile(sourceFile, 'source_path')
const current = readFile(targetFile, 'target_path')
const updated = convert(sample, current)

if (updated === current) {
log(`unchanged: ${target}`)
} else if (reportOnly) {
log(`would update: ${target}`)
} else {
fs.writeFileSync(targetFile, updated)
log(`updated: ${target}`)
}
}

if (stopAfterProcessing) this.stop(0)
})
}

function readFile (file, key) {
try {
return fs.readFileSync(file, 'utf8')
} catch (err) {
throw new Error(`config-sample-to-adoc: cannot read ${key} ${file}: ${err.message}`)
}
}
Loading
Loading