diff --git a/.forgejo/workflows/app-tests-live.yml b/.forgejo/workflows/app-tests-live.yml deleted file mode 100644 index 2dd74174b..000000000 --- a/.forgejo/workflows/app-tests-live.yml +++ /dev/null @@ -1,50 +0,0 @@ -# app-tests-live.yml — doriath caller for the LIVE-NC reusable workflow. -# -# Boots a real, seeded Nextcloud (db + NC + openregister deployed/enabled + -# doriath deployed/enabled), then runs the deep Playwright e2e -# (tests/e2e/workflows/) and the Newman API-contract suite against it as HARD -# gates — a PR can no longer go green while those flows are broken. -# -# This is the Forgejo/Codeberg counterpart to openregister's app-tests-live.yml -# reference rig, generalised per GAP-2 (arm CI live-gating). The reusable -# tests-live.yml is byte-identical across the fleet; only these inputs differ. -# Additive — sits alongside app-tests.yml (bare gates: phpunit-unit, l10n) and -# the release / pre-merge workflows, none of which are touched. -# -# Gate status for doriath (see TESTING-CI-ROLLOUT.md for the evidence table): -# deep-e2e: true — OR object API by slug, run-id-prefixed -# newman: true — 44 assertions, 0 fail live (isolated) - -name: app-tests-live - -on: - pull_request: - branches: - - development - - main - - beta - push: - branches: - - development - - main - workflow_dispatch: - -permissions: - contents: read - -jobs: - live: - uses: ./.forgejo/workflows/tests-live.yml - with: - app-id: doriath - is-openregister: false - run-e2e: true - run-newman: true - # NON-GATING (GAP-5): runs the visual-regression project + uploads - # snapshots/diffs as an artifact. Committed baselines are dev-container - # native, so they won't byte-match the CI Linux runner until regenerated - # in-CI (see tests-live.yml run-visual caveat) — continue-on-error. - run-visual: true - # Feature apps use the per-app Newman entrypoint (self-seeding collection), - # not openregister's multi-collection orchestrator. - newman-entrypoint: tests/integration/run-newman.sh diff --git a/.forgejo/workflows/app-tests.yml b/.forgejo/workflows/app-tests.yml deleted file mode 100644 index 97d6e1871..000000000 --- a/.forgejo/workflows/app-tests.yml +++ /dev/null @@ -1,34 +0,0 @@ -# app-tests.yml — doriath caller for the reusable feature-test workflow. -# -# Runs the PHASE-5 testing layers on every PR to the protected branches. -# phpunit-unit + l10n-check HARD-GATE; e2e/newman are scaffolded and opt-in -# (run-e2e / run-newman) until the NC service container is wired (see tests.yml -# TODOs). Additive — sits alongside the existing release/quality workflows. - -name: app-tests - -on: - pull_request: - branches: - - development - - main - - beta - workflow_dispatch: - inputs: - run-e2e: - type: boolean - default: false - run-newman: - type: boolean - default: false - -permissions: - contents: read - -jobs: - tests: - uses: ./.forgejo/workflows/tests.yml - with: - app-id: doriath - run-e2e: ${{ github.event.inputs.run-e2e == 'true' }} - run-newman: ${{ github.event.inputs.run-newman == 'true' }} diff --git a/.forgejo/workflows/pre-merge-check-strict.yaml b/.forgejo/workflows/pre-merge-check-strict.yaml deleted file mode 100644 index 863f2b03e..000000000 --- a/.forgejo/workflows/pre-merge-check-strict.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# Pre-merge quality gate — enforced lint + phpcs + all Hydra gates on every PR. -# Required status check on protected branches. -# -# Runner/container mirror the proven release-semrel workflow: codeberg-medium + -# official php:8.3-cli + a base-tooling step. The old code.forgejo.org/oci/ci-php:8.3 -# image 404s ("manifest unknown"), which fast-failed every run at container-pull. -# -# The gate runs `composer lint` + `composer phpcs` directly: check:strict's -# psalm/phpstan/phpmd/test:all are wrapped in `|| echo skipping` so they never -# affect pass/fail (ADR-022 parks static analysis), and running them on the medium -# runner OOMs it. lint+phpcs is the identical enforced gate, fast and deterministic. - -name: pre-merge-check-strict - -on: - pull_request: - branches: - - development - - main - - beta - -jobs: - quality-gates: - runs-on: codeberg-medium - container: - image: php:8.3-cli - timeout-minutes: 15 - steps: - - name: Install base tooling - run: | - apt-get update - apt-get install -y --no-install-recommends \ - git curl ca-certificates gnupg jq unzip zip \ - libzip-dev libpng-dev python3 - # Node is required by actions/checkout@v4 (a JS action) which runs - # inside this php:8.3-cli container; the stock image ships no node. - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - apt-get install -y --no-install-recommends nodejs - docker-php-ext-install -j"$(nproc)" zip gd - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - - - name: Checkout PR - uses: https://github.com/actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install composer deps - run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-reqs - - - name: Run lint + phpcs (the enforced gate) - run: | - composer lint - composer phpcs - - - name: Clone Hydra (for gate runner) - uses: https://github.com/actions/checkout@v4 - with: - repository: Conduction/hydra - ref: development - path: .hydra - - - name: Run all Hydra gates (diff-scoped per ADR-020) - run: | - git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} || true - bash .hydra/scripts/run-hydra-gates.sh --scope-to-diff --base origin/${{ github.base_ref }} . - - - name: Gate-19 e2e coverage report (informational) - if: always() - run: | - python3 .hydra/scripts/lib/check_e2e_coverage.py . --mode report || true diff --git a/.forgejo/workflows/tests-live.yml b/.forgejo/workflows/tests-live.yml deleted file mode 100644 index d1a3ed637..000000000 --- a/.forgejo/workflows/tests-live.yml +++ /dev/null @@ -1,327 +0,0 @@ -# tests-live.yml — reusable LIVE-NC feature-test workflow (PHASE-5 NC-in-CI). -# -# This is the GATING counterpart to tests.yml: where tests.yml runs the bare -# layers (phpunit-unit, l10n) that need no service container, this workflow -# boots a real, seeded Nextcloud and runs the two layers that DO need one — -# the deep Playwright e2e (tests/e2e/workflows/) and the Newman API-contract -# suite — so a PR can no longer go green while those flows are broken. -# -# It generalises openregister's reference rig -# (.github/workflows/api-test-coverage.yml + .github/docker-compose.ci.yml), -# which already boots NC + Postgres, deploys + enables OpenRegister, waits for -# the API and runs Newman. The only per-app knobs are `app-id` and whether the -# app is itself openregister (the data backend) or a feature app that needs -# openregister enabled ALONGSIDE it. -# -# Per-app PREREQUISITE (the common seeding step): -# The deep e2e + Newman suites must be SELF-SEEDING — each spec/collection -# creates the OR register + schema + objects it asserts on in its -# beforeAll/setUp and tears them down in afterAll/teardown. openregister's -# tests/e2e/workflows/object-lifecycle-workflows.spec.ts and the Newman -# collections already do this. A feature app whose suites assume a -# pre-imported OR register-config must EITHER add an `occ` seed step here -# (import the app's register-config, e.g. via its Repair step or -# `occ :import-config`) OR make the suites self-seed. Until one of -# those is true, keep run-e2e/run-newman false for that app. -# -# Runner label `docker` (needs a docker daemon to boot the compose stack) + -# the short `https://code.forgejo.org/actions/...@v4` `uses:` form follow the -# fleet convention. Additive — does not touch tests.yml, pre-merge-check-strict -# or any release workflow. - -name: tests-live - -on: - workflow_call: - inputs: - app-id: - description: "App id (matches package.json name + composer namespace + custom_apps dir)." - required: true - type: string - node-version: - required: false - type: string - default: "20" - php-version: - required: false - type: string - default: "8.3" - is-openregister: - description: "True when app-id IS openregister (the data backend). Feature apps leave this false; openregister is enabled alongside them." - required: false - type: boolean - default: false - run-e2e: - description: "Run the deep Playwright e2e (tests/e2e/workflows/) against the live NC. GATING when true." - required: false - type: boolean - default: false - run-newman: - description: "Run the Newman API-contract suite against the live NC. GATING when true." - required: false - type: boolean - default: false - run-visual: - description: >- - Run the Playwright visual-regression project (tests/e2e/visual/, GAP-5) - against the live NC. NON-GATING by design (continue-on-error). PLATFORM - CAVEAT: PNG baselines are host-font/GPU specific, so committed - dev-container baselines will NOT byte-match a CI Linux runner. On the - first CI run the baselines must be regenerated in-CI (download the - uploaded visual-snapshots artifact and commit it) before this step can - be made gating. Until then it reports diffs as an artifact only. - required: false - type: boolean - default: false - newman-entrypoint: - description: "Path to the Newman runner. openregister uses the orchestrator; feature apps use tests/integration/run-newman.sh." - required: false - type: string - default: "tests/newman/run-all.sh" - newman-collections: - description: "COLLECTIONS subset passed to the orchestrator (self-seeding domains only; excludes fixtures that assume dev-container state)." - required: false - type: string - default: "crud graphql relations auth-matrix error-matrix referential-integrity" - -permissions: - contents: read - -jobs: - # --------------------------------------------------------------------------- - # LIVE GATE — boot seeded NC, deploy OR (+ the app), run deep e2e + Newman. - # - # A single job boots the stack once and runs both suites against it (cheaper - # than two stacks; both are read-mostly + self-seeding so they don't collide - # — e2e prefixes its fixtures with a run-id, Newman teardown-cleans its own). - # --------------------------------------------------------------------------- - live-nc: - name: Live NC e2e + Newman (${{ inputs.app-id }}) - if: ${{ inputs.run-e2e || inputs.run-newman || inputs.run-visual }} - runs-on: docker - timeout-minutes: 40 - permissions: - contents: read - env: - APP_ID: ${{ inputs.app-id }} - COMPOSE_FILE: .github/docker-compose.ci.yml - steps: - - name: Checkout - uses: https://code.forgejo.org/actions/checkout@v4 - - - name: Set up PHP - uses: https://github.com/shivammathur/setup-php@v2 - with: - php-version: ${{ inputs.php-version }} - tools: composer:v2 - coverage: none - - - name: Set up Node.js - uses: https://code.forgejo.org/actions/setup-node@v4 - with: - node-version: ${{ inputs.node-version }} - - - name: Install composer deps (production) - run: composer install --no-dev --no-interaction --no-progress --prefer-dist - - - name: Install npm deps + Newman - run: | - npm ci --no-audit --no-fund || npm install --no-audit --no-fund - npm install -g newman - - - name: Boot CI stack (db + Nextcloud) - # Reuses openregister's reference compose (db + NC, named volume only). - # A feature app that does not ship its own compose should commit a copy - # of openregister/.github/docker-compose.ci.yml. - run: docker compose -f "$COMPOSE_FILE" up -d - - - name: Wait for Nextcloud to be installed - run: | - for i in $(seq 1 60); do - if docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ status" 2>/dev/null | grep -q "installed: true"; then - echo "Nextcloud installed and ready" - break - fi - echo "Waiting for Nextcloud installation... ($i/60)" - sleep 5 - done - docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ status" - - - name: Deploy OpenRegister (data backend) - # The app under test stores its objects in OpenRegister, so OR must be - # deployed+enabled regardless of which app we're testing. When the app - # UNDER TEST *is* openregister, this single deploy covers it. - run: | - if [ "${{ inputs.is-openregister }}" = "true" ]; then - OR_SRC="." - else - # Feature apps build against a sibling openregister checkout in the - # same apps-extra tree. CI clones only this repo, so fetch OR's - # release build instead. - OR_SRC="$RUNNER_TEMP/openregister" - git clone --depth 1 https://codeberg.org/Conduction/openregister.git "$OR_SRC" - ( cd "$OR_SRC" && composer install --no-dev --no-interaction --no-progress --prefer-dist ) - fi - docker exec nextcloud mkdir -p /var/www/html/custom_apps/openregister - tar --exclude='.git' --exclude='node_modules' --exclude='.claude' \ - --exclude='tests/e2e/playwright-report' --exclude='tests/e2e/test-results' \ - -C "$OR_SRC" -cf - . \ - | docker exec -i nextcloud tar -xf - -C /var/www/html/custom_apps/openregister - docker exec nextcloud chown -R www-data:www-data /var/www/html/custom_apps/openregister - docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:enable openregister" - - - name: Deploy app under test - # Skipped when the app IS openregister (already deployed above). - if: ${{ inputs.is-openregister == false }} - run: | - docker exec nextcloud mkdir -p "/var/www/html/custom_apps/$APP_ID" - tar --exclude='.git' --exclude='node_modules' --exclude='.claude' \ - --exclude='tests/e2e/playwright-report' --exclude='tests/e2e/test-results' \ - -cf - . \ - | docker exec -i nextcloud tar -xf - -C "/var/www/html/custom_apps/$APP_ID" - docker exec nextcloud chown -R www-data:www-data "/var/www/html/custom_apps/$APP_ID" - docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:enable $APP_ID" - docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:list" | grep "$APP_ID" - - - name: Seed OR register-config (per-app prerequisite) - # SELF-SEEDING suites need nothing here. An app whose deep-e2e / Newman - # assumes a pre-imported register-config should import it now, e.g.: - # docker exec nextcloud su -s /bin/bash www-data -c \ - # "php /var/www/html/occ $APP_ID:import-config" # if the app ships one - # or trigger the app's Repair step (most Conduction apps import their - # register via lib/Repair/Initialize*.php on app:enable — already done - # by the enable step above). Left as an explicit, documented hook. - run: | - echo "Seeding: openregister's own suites are self-seeding (fixtures create" - echo "register+schema+objects per run-id, torn down in afterAll/teardown)." - echo "Feature apps: add the register-config import here (see step comment)." - - - name: Wait for the API to respond - # app:enable returns before the repair/magic-mapping step + PHP-FPM - # opcache warm-up finish; poll the OR registers endpoint until it - # returns a real HTTP status (not connection-level 000). - run: | - for i in $(seq 1 60); do - code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \ - -u admin:admin \ - http://localhost:8080/index.php/apps/openregister/api/registers \ - 2>/dev/null | tr -d '\n') - if [ -n "$code" ] && [ ${#code} -eq 3 ] \ - && [ "$code" -ge 100 ] 2>/dev/null && [ "$code" -lt 500 ]; then - echo "OpenRegister API responding (HTTP $code, attempt $i)" - sleep 5 - break - fi - echo "Waiting for API... (attempt $i/60, last='$code')" - sleep 2 - done - - - name: Mint admin storageState + run deep e2e (GATING) - # The app's playwright global-setup logs into NC once and persists the - # cookie jar to tests/e2e/.auth/admin.json (the storageState the - # workflows spec consumes). We run ONLY tests/e2e/workflows here — the - # deep, data-dependent layer — against the live, self-seeded NC. - if: ${{ inputs.run-e2e }} - env: - NEXTCLOUD_URL: http://localhost:8080 - NC_ADMIN_USER: admin - NC_ADMIN_PASS: admin - OR_USER: admin - OR_PASS: admin - CI: "true" - run: | - npx playwright install --with-deps chromium - npx playwright test tests/e2e/workflows - - - name: Run visual-regression project (NON-GATING — GAP-5) - # Visual baselines are rendered against the local dev container; a CI - # Linux runner uses a different font stack + GPU so the committed PNGs - # will not byte-match here. This step is therefore NON-GATING - # (continue-on-error) and exists to (a) surface visual diffs as an - # artifact and (b) let a maintainer regenerate CI-native baselines: - # download the `visual-snapshots-` artifact from a run with - # PLAYWRIGHT_UPDATE=1 and commit it, then drop continue-on-error to - # make the step gating in the CI environment. - if: ${{ inputs.run-visual }} - continue-on-error: true - env: - NEXTCLOUD_URL: http://localhost:8080 - NC_ADMIN_USER: admin - NC_ADMIN_PASS: admin - OR_USER: admin - OR_PASS: admin - CI: "true" - run: | - npx playwright install --with-deps chromium - if [ "${PLAYWRIGHT_UPDATE:-0}" = "1" ]; then - echo "Regenerating CI-native visual baselines (--update-snapshots)…" - npx playwright test --project visual --update-snapshots || true - else - npx playwright test --project visual || true - fi - - - name: Upload visual snapshots + diffs (NON-GATING) - if: ${{ inputs.run-visual }} - continue-on-error: true - uses: https://code.forgejo.org/actions/upload-artifact@v4 - with: - name: visual-snapshots-${{ inputs.app-id }} - path: | - tests/e2e/visual/**/*-snapshots/** - tests/e2e/test-results/** - retention-days: 14 - if-no-files-found: ignore - - - name: Run Newman API-contract suite (GATING) - if: ${{ inputs.run-newman }} - env: - BASE_URL: http://localhost:8080 - ADMIN_USER: admin - ADMIN_PASSWORD: admin - CONTAINER_NAME: nextcloud - NEWMAN_RUNNER: host - FAIL_FAST: "0" - COLLECTIONS: ${{ inputs.newman-collections }} - run: bash "${{ inputs.newman-entrypoint }}" - - - name: Upload Playwright report - if: always() - uses: https://code.forgejo.org/actions/upload-artifact@v4 - with: - name: playwright-report-${{ inputs.app-id }} - path: | - tests/e2e/playwright-report/ - tests/e2e/test-results/ - retention-days: 14 - if-no-files-found: ignore - - - name: Upload Newman reports - if: always() - uses: https://code.forgejo.org/actions/upload-artifact@v4 - with: - name: newman-reports-${{ inputs.app-id }} - path: | - tests/newman/reports/ - newman-*.json - retention-days: 14 - if-no-files-found: ignore - - - name: Collect docker logs on failure - if: failure() - run: | - mkdir -p ci-logs - docker compose -f "$COMPOSE_FILE" logs --no-color > ci-logs/docker-compose.log 2>&1 || true - docker exec nextcloud cat /var/www/html/data/nextcloud.log > ci-logs/nextcloud.log 2>&1 || true - - - name: Upload docker logs - if: failure() - uses: https://code.forgejo.org/actions/upload-artifact@v4 - with: - name: ci-logs-${{ inputs.app-id }} - path: ci-logs/ - retention-days: 14 - if-no-files-found: ignore - - - name: Tear down CI stack - if: always() - run: docker compose -f "$COMPOSE_FILE" down -v diff --git a/.forgejo/workflows/tests.yml b/.forgejo/workflows/tests.yml deleted file mode 100644 index f6d766858..000000000 --- a/.forgejo/workflows/tests.yml +++ /dev/null @@ -1,274 +0,0 @@ -# tests.yml — reusable feature-test workflow for Conduction NC apps. -# -# PHASE-5 CI enforcement: runs the testing layers we built so a PR can't go -# green while a feature is broken. This is a `workflow_call` reusable: a thin -# per-app caller (app-tests.yml) invokes it with the app id. Copy the pair -# (this file + app-tests.yml) into any sibling app to roll the pattern out — -# the only per-app knob is `app-id`. -# -# Layers, and what gates today: -# • phpunit-unit — HARD GATE. tests/Unit + tests/unit via phpunit-unit.xml. -# The bootstrap runs standalone (vendor OCP stubs, no NC), -# so this needs no service container. -# • l10n-check — HARD GATE. tests/l10n/check-l10n.js asserts every -# t('', '...') / n(...) source string is present in -# l10n/en.json (the i18n-extraction-drift guard). Pure -# Node, no NC. -# • e2e-deep — SCAFFOLDED (non-gating). Playwright tests/e2e/workflows/ -# need a live, seeded NC. See the TODO in that job. -# • newman — SCAFFOLDED (non-gating). tests/integration Postman -# collections via run-newman.sh need a live NC. See TODO. -# -# Runner labels + reusable `uses:` forms follow the fleet convention -# (codeberg-small / short Conduction/.github@main form). Additive — does not -# touch pre-merge-check-strict.yaml or any release workflow. - -name: tests - -on: - workflow_call: - inputs: - app-id: - description: "App id (matches package.json name + composer namespace)." - required: true - type: string - php-version: - required: false - type: string - default: "8.3" - node-version: - required: false - type: string - default: "20" - unit-gating: - description: "Fail the workflow on unit-test failures. Set false for apps whose tests/Unit suite is not yet green on baseline (e.g. openregister — see TESTING-CI-ROLLOUT.md)." - required: false - type: boolean - default: true - run-e2e: - description: "Run the scaffolded deep-e2e job (needs NC service — see TODO)." - required: false - type: boolean - default: false - run-newman: - description: "Run the scaffolded Newman job (needs NC service — see TODO)." - required: false - type: boolean - default: false - -permissions: - contents: read - -jobs: - # --------------------------------------------------------------------------- - # HARD GATE 1 — PHPUnit unit suite (no NC needed; vendor OCP stubs). - # --------------------------------------------------------------------------- - phpunit-unit: - name: PHPUnit unit (${{ inputs.app-id }}) - runs-on: codeberg-medium - container: - image: php:8.3-cli - steps: - - name: Install base tooling - run: | - apt-get update - apt-get install -y --no-install-recommends \ - git curl ca-certificates gnupg jq unzip zip \ - libzip-dev libpng-dev python3 - # Node is required by actions/checkout@v4 (a JS action) which runs - # inside this php:8.3-cli container; the stock image ships no node. - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - apt-get install -y --no-install-recommends nodejs - docker-php-ext-install -j"$(nproc)" zip gd - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - - - name: Checkout - uses: https://github.com/actions/checkout@v4 - - - name: Install composer deps - run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-reqs - - - name: Run unit suite (phpunit-unit.xml) - # unit-gating=false reports failures without failing the job, for apps - # carrying pre-existing tests/Unit debt (see TESTING-CI-ROLLOUT.md). - continue-on-error: false - run: ./vendor/bin/phpunit --configuration phpunit-unit.xml --no-coverage --colors=never - - # --------------------------------------------------------------------------- - # HARD GATE 2 — l10n extraction-drift check (no NC needed; pure Node). - # --------------------------------------------------------------------------- - l10n-check: - name: l10n extraction check (${{ inputs.app-id }}) - runs-on: codeberg-medium - container: - image: node:${{ inputs.node-version }} - steps: - - name: Checkout - uses: https://github.com/actions/checkout@v4 - - - name: Assert every t() source string is in l10n/en.json - run: node tests/l10n/check-l10n.js - - # --------------------------------------------------------------------------- - # HARD GATE 3 — frontend unit suite (Vitest, OFFLINE; no NC needed). - # - # Runs the pure-logic Vitest suite under tests/vitest/** (Pinia store - # state transitions, util/formatter calc, form-validation mappers, and any - # offline component mounts). These need no DOM/NC runtime — @nextcloud/* and - # @conduction/nextcloud-vue are aliased to deterministic stubs in - # vitest.config.js. Always gating. - # --------------------------------------------------------------------------- - frontend-unit: - name: Frontend unit (Vitest — ${{ inputs.app-id }}) - runs-on: codeberg-medium - container: - image: node:${{ inputs.node-version }} - steps: - - name: Checkout - uses: https://github.com/actions/checkout@v4 - - - name: Install npm deps - run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps - - - name: Run Vitest unit suite - run: npm run test:unit - - # --------------------------------------------------------------------------- - # COVERAGE GATE A — PHPUnit COVERAGE RATCHET (PCOV clover line coverage). - # Fails a PR that drops backend coverage below tests/.coverage-baseline.json - # `phpunit` minus tolerance. Inherits continue-on-error from unit-gating so a - # red unit suite records-but-does-not-block. Seeds on first --update run when - # the baseline is null. See TESTING-CI-ROLLOUT.md "Coverage ratchet". - # --------------------------------------------------------------------------- - phpunit-coverage-ratchet: - name: PHPUnit coverage ratchet (${{ inputs.app-id }}) - runs-on: codeberg-medium - container: - image: php:8.3-cli - continue-on-error: false - steps: - - name: Install base tooling - run: | - apt-get update - apt-get install -y --no-install-recommends \ - git curl ca-certificates gnupg jq unzip zip \ - libzip-dev libpng-dev python3 - # Node is required by actions/checkout@v4 (a JS action) which runs - # inside this php:8.3-cli container; the stock image ships no node. - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - apt-get install -y --no-install-recommends nodejs - docker-php-ext-install -j"$(nproc)" zip gd - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - - - name: Checkout - uses: https://github.com/actions/checkout@v4 - - - name: Ensure a coverage driver (PCOV) - run: | - if ! php -m | grep -qiE 'pcov|xdebug'; then - (pecl install pcov && docker-php-ext-enable pcov) \ - || echo "WARN: could not install pcov — coverage step may report no driver" - fi - php -m | grep -qiE 'pcov|xdebug' && echo "coverage driver: present" \ - || echo "coverage driver: ABSENT (ratchet will no-op; see TESTING-CI-ROLLOUT.md)" - - - name: Install composer deps - run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-reqs - - - name: Run unit suite WITH coverage (clover) - run: | - php -d pcov.enabled=1 -d pcov.directory=lib \ - ./vendor/bin/phpunit --configuration phpunit-unit.xml \ - --coverage-clover coverage/clover.xml --colors=never || true - test -f coverage/clover.xml || { echo "no clover.xml (driver absent?) — skipping ratchet"; exit 0; } - - - name: Coverage ratchet (fail on drop) - run: | - test -f coverage/clover.xml || exit 0 - bash tests/coverage-ratchet.sh phpunit coverage/clover.xml - - # --------------------------------------------------------------------------- - # COVERAGE GATE B — FRONTEND COVERAGE RATCHET (Vitest v8, src/** line coverage). - # Fails a PR that drops frontend coverage below baseline `vitest` minus - # tolerance. Seeds on first --update run when the baseline is null. - # --------------------------------------------------------------------------- - frontend-coverage-ratchet: - name: Frontend coverage ratchet (${{ inputs.app-id }}) - runs-on: codeberg-medium - container: - image: node:${{ inputs.node-version }} - steps: - - name: Checkout - uses: https://github.com/actions/checkout@v4 - - - name: Install npm deps (+ coverage-v8) - run: | - npm ci --legacy-peer-deps || npm install --legacy-peer-deps - VITEST_VER="$(node -e "console.log(require('./node_modules/vitest/package.json').version)")" - npm install --no-save --legacy-peer-deps "@vitest/coverage-v8@${VITEST_VER}" - - - name: Run Vitest WITH coverage (json-summary over src/**) - run: | - npx vitest run --coverage --coverage.provider=v8 \ - --coverage.reporter=json-summary --coverage.reporter=text-summary \ - --coverage.include='src/**' \ - --coverage.reportsDirectory=coverage-vitest || true - test -f coverage-vitest/coverage-summary.json \ - || { echo "no coverage-summary.json — vitest coverage unavailable; skipping ratchet"; exit 0; } - - - name: Coverage ratchet (fail on drop) - run: | - test -f coverage-vitest/coverage-summary.json || exit 0 - bash tests/coverage-ratchet.sh vitest coverage-vitest/coverage-summary.json - - # --------------------------------------------------------------------------- - # SCAFFOLD — deep e2e (Playwright tests/e2e/workflows/). Opt-in via run-e2e. - # - # TODO(nc-in-ci): wire a live Nextcloud before flipping this to gating: - # 1. Boot db + NC (openregister ships .github/docker-compose.ci.yml; add a - # sibling compose for feature apps, or reuse the OR stack + enable this - # app + its OR register/schema fixtures). - # 2. Deploy the working tree into custom_apps/ and `occ app:enable` - # (+ openregister, the data backend). - # 3. Seed the deep-e2e fixtures (tests/e2e/workflows/*fixture*.ts seed the - # OR objects each workflow asserts on). - # 4. `npx playwright install --with-deps chromium` then - # `npm run test:e2e -- tests/e2e/workflows`. - # Until then this job documents the command and is non-gating. - # --------------------------------------------------------------------------- - e2e-deep: - name: Deep e2e (scaffold — needs NC) - if: ${{ inputs.run-e2e }} - runs-on: docker - steps: - - name: Checkout - uses: https://github.com/actions/checkout@v4 - - - name: TODO — boot seeded NC, then run deep e2e - run: | - echo "Deep e2e requires a live, seeded Nextcloud (see job header TODO)." - echo "Local: npm ci && npm run test:e2e:install && npm run test:e2e -- tests/e2e/workflows" - echo "Skipping in CI until the NC service container is wired." - - # --------------------------------------------------------------------------- - # SCAFFOLD — Newman API-contract (tests/integration/*.postman_collection.json). - # Opt-in via run-newman. - # - # TODO(nc-in-ci): same live-NC prerequisite as e2e-deep. The runner script - # (tests/integration/run-newman.sh) is collection-self-seeding and runnable - # locally today: `bash tests/integration/run-newman.sh`. openregister uses - # the orchestrator at tests/newman/run-all.sh instead. - # --------------------------------------------------------------------------- - newman: - name: Newman API contract (scaffold — needs NC) - if: ${{ inputs.run-newman }} - runs-on: docker - steps: - - name: Checkout - uses: https://github.com/actions/checkout@v4 - - - name: TODO — boot NC, then run Newman - run: | - echo "Newman requires a live Nextcloud serving the app (see job header TODO)." - echo "Local: bash tests/integration/run-newman.sh # OR: bash tests/newman/run-all.sh" - echo "Skipping in CI until the NC service container is wired." diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bd35c7ff5..6d8325f0d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,7 @@ updates: directory: "/" schedule: interval: "weekly" + target-branch: "development" open-pull-requests-limit: 10 cooldown: default-days: 2 diff --git a/.github/docker-compose.ci.yml b/.github/docker-compose.ci.yml index 7d452823d..7ff9713af 100644 --- a/.github/docker-compose.ci.yml +++ b/.github/docker-compose.ci.yml @@ -20,7 +20,7 @@ volumes: services: db: image: pgvector/pgvector:pg16 - container_name: doriath-ci-db + container_name: keepiq-ci-db environment: POSTGRES_DB: nextcloud POSTGRES_USER: nextcloud @@ -62,4 +62,4 @@ services: networks: default: - name: doriath-ci-network + name: keepiq-ci-network diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 6f44c7b3a..ae1c70870 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -1,6 +1,6 @@ name: CLI Release -# Builds the doriath-cli single static binary for every supported platform. +# Builds the keepiq-cli single static binary for every supported platform. # On a tag push it also uploads the artifacts to the GitHub release. The CLI is # stdlib-only Go, so the matrix is a plain cross-compile — no PHP/Node needed. @@ -60,12 +60,12 @@ jobs: run: | ext="" [ "$GOOS" = "windows" ] && ext=".exe" - out="doriath-${GOOS}-${GOARCH}${ext}" + out="keepiq-${GOOS}-${GOARCH}${ext}" go build -trimpath -ldflags "-s -w -X main.version=${GITHUB_REF_NAME}" -o "$out" . echo "ARTIFACT=cli/$out" >> "$GITHUB_ENV" - uses: actions/upload-artifact@v4 with: - name: doriath-${{ matrix.target.goos }}-${{ matrix.target.goarch }} + name: keepiq-${{ matrix.target.goos }}-${{ matrix.target.goarch }} path: ${{ env.ARTIFACT }} - name: Attach to release if: startsWith(github.ref, 'refs/tags/cli-v') diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 94cdd45ed..e7602f898 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -62,7 +62,47 @@ on: # (`quality-feature/x` for both events, exactly as before) and gives the two # default branches' push runs a lane of their own. concurrency: - group: quality-${{ github.head_ref || github.ref_name }}${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'development')) && '-push' || '' }} + # SUFFIXED BY EVENT NAME, not just by `-push`. + # + # The previous expression gave a push on `development` its own lane + # (`-push`) but left EVERYTHING ELSE sharing `quality-development` — and + # that is not a quiet lane: `Sync to Beta` keeps a PR open whose head_ref + # IS `development`, so its run computes the same group and is re-triggered + # on every merge. + # + # A `workflow_dispatch` therefore shared a group with that PR and was + # cancelled by it. Measured on shillinq 2026-08-21: dispatch 32487948678 + # cancelled by pull_request run 32490160836 (head_branch `development`). + # A run someone deliberately asked for could essentially never complete. + # + # That reaches past ad-hoc verification: the fleet gate-drift sweep + # (.github#523) dispatches per app with `--ref development`, because + # `schedule:` cannot choose a branch. Under the old group those runs are + # cancelled and report neither pass nor fail — and a routine that produces + # no verdict is indistinguishable from one that never ran. + # + # This is hermiq's form, already live there. Pull requests keep the bare + # group (so a PR still supersedes its own earlier run); push, dispatch and + # schedule each get their own lane. + # + # THE BRANCH RESTRICTION IS GONE, because it contradicted the sentence above. + # + # The suffix used to apply only when `ref_name` was `main` or `development`, + # so on every OTHER branch push and pull_request computed the SAME group — + # and `cancel-in-progress` made them kill each other. That became reachable + # when the push allow-list widened on 2026-08-14 to include `feat/**`, + # `fix/**`, `perf/**`, `refactor/**` and `chore/**`: those branches now get + # both a push run and a pull_request run for one commit. + # + # `quality / Quality Report` is a `needs:`-gated aggregator and reports + # FAILURE when its dependencies are CANCELLED, so the collision shows up as a + # red gate on a PR that was never actually evaluated — and re-running collides + # the same way. Measured on openregister#2821: a push run left queued and a + # pull_request run cancelled, 18 seconds apart, on one commit. + # + # A branch name is not a unique lane when two event types can each produce a + # run for it, so the event is now always part of the key. + group: quality-${{ github.head_ref || github.ref_name }}${{ github.event_name != 'pull_request' && format('-{0}', github.event_name) || '' }} cancel-in-progress: true # Permission CEILING for the called quality pipeline. GitHub statically @@ -81,14 +121,14 @@ jobs: quality: uses: ConductionNL/.github/.github/workflows/quality.yml@main with: - app-name: doriath + app-name: keepiq php-version: "8.3" php-test-versions: '["8.3", "8.4"]' # Order matters: the PHPUnit matrix uses the whole list, but the E2E # (Playwright), Newman and Journeydoc jobs all check out # `fromJSON(nextcloud-test-refs)[0]` as their single server. That server # has to be one OpenRegister can load, because `additional-apps` below - # installs it and doriath's AppHost integration delegates to it — + # installs it and keepiq's AppHost integration delegates to it — # OpenRegister's lib/ContextChat/ContentProvider.php implements # `OCP\ContextChat\IContentProvider`, which exists in stable32 and NOT in # stable31 (verified: raw.githubusercontent.com returns 404 for the @@ -123,8 +163,8 @@ jobs: enable-eslint: true enable-phpunit: true enable-newman: true - # Doriath adopts OpenRegister's AppHost engine in lib/AppInfo/Application.php - # (ADR-040 / ADR-022) and ships lib/Settings/doriath_register.json, so + # Keepiq adopts OpenRegister's AppHost engine in lib/AppInfo/Application.php + # (ADR-040 / ADR-022) and ships lib/Settings/keepiq_register.json, so # OpenRegister must be present in the test instance — otherwise # SettingsService::isOpenRegisterAvailable() is false, InitializeSettings # skips the register import with a warning, and the app boots without the @@ -157,7 +197,7 @@ jobs: # upload-artifact steps actually look. enable-playwright: true playwright-test-path: tests/e2e - # Doriath's nine workflow specs UNLOCK the vault with the development + # Keepiq's nine workflow specs UNLOCK the vault with the development # master password seeded by lib/Repair/SeedDevelopmentData.php — and that # repair step returns immediately unless the `debug` system config is # true, which `occ maintenance:install` does not set. A stock CI install @@ -166,7 +206,7 @@ jobs: # register through OpenRegister's admin API, and fails loudly if the # suite / secrets / schemas still aren't there. # cwd for this step is the Nextcloud server root. - playwright-seed-command: 'bash apps/doriath/tests/e2e/ci-seed.sh' + playwright-seed-command: 'bash apps/keepiq/tests/e2e/ci-seed.sh' # ── Frontend Check legs ────────────────────────────────────────────── # `frontend-checks` defaults to `[]`, and an empty list means the shared @@ -189,7 +229,17 @@ jobs: # frontend files in scope. This repo has TWO documentation trees and both # are excluded by .prettierignore — `docs/` and the separate `docusaurus/` # site, which has its own package.json and its own toolchain. - frontend-checks: '["check:manifest", "test:l10n", "format"]' + # `check:l10n-js` regenerates l10n/.js from the JSON catalogue and + # fails when the committed file is stale. Nextcloud serves ONLY the JS half + # to a browser — raw JSON out of an app directory is a 404 — so a catalogue + # that exists only as JSON renders English in the entire UI while every + # server-rendered string is translated, with no error anywhere. + # `check:schema-l10n` is a RATCHET, not a gate. Every string inside a form + # comes from the schema and is a key in THIS app's catalogue; an absent key + # renders the English source inside an otherwise translated form, silently. + # The fleet had 30,459 such strings, so this records the current count and + # fails only when it GROWS — burning it down stays an ordinary PR. + frontend-checks: '["check:manifest", "test:l10n", "format", "check:l10n-js", "check:schema-l10n"]' # ── Coverage ratchet ───────────────────────────────────────────────── # `enable-coverage-guard` defaults to FALSE, which is why both diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index d7f3d1fbe..ffa8352d8 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -2,12 +2,55 @@ name: Documentation on: push: - branches: [documentation] + branches: [development] pull_request: - branches: [documentation] + branches: [development] jobs: deploy: uses: ConductionNL/.github/.github/workflows/documentation.yml@main + # A reusable workflow receives NO secrets by default. Without this block + # `secrets.CF_API_TOKEN` is empty inside the callee, its "Publish to the + # Cloudflare Worker" step skips itself on its own guard, and the run + # finishes GREEN having written only gh-pages — which nothing serves. The + # live site never changes and no check goes red to say so. + # + # Mapped explicitly rather than `secrets: inherit`, because `inherit` + # hands the callee EVERY secret this repo holds — signing cert and key, + # appstore token, deploy keys — for the sake of two Cloudflare values. + # This way only those two cross the boundary. + # + # The exposure above is the ONLY reason for the explicit mapping. The + # names are the same on both sides: the org secrets really are + # `CF_API_TOKEN` / `CF_ACCOUNT_ID` — the names ConductionNL/.github's own + # deploy-docs.yml reads directly, and the names the callee declares under + # `workflow_call.secrets`. + # + # This block used to read `secrets.CLOUDFLARE_API_TOKEN` / + # `secrets.CLOUDFLARE_ACCOUNT_ID`, which are not secrets anywhere in this + # org. Mapping from a name that does not exist is NOT an error — it + # yields an empty string — so the callee's publish step skipped itself on + # its own guard and the run still finished green. Measured on planninq + # run 32760529026: "Publish to the Cloudflare Worker" SKIPPED, the log + # showing `CF_API_TOKEN:` with no value. + secrets: + CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} + CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} with: - cname: doriath.conduction.nl + # `keepiq.conduction.nl` resolves as of 2026-08-23 — attached as a second + # custom domain on the SAME `doriath-docs` worker that serves + # `doriath.conduction.nl`. Both hosts answer, so nothing goes dark either + # way. docs-hosts must list BOTH: wrangler reconciles the worker's + # triggers against it, so a host omitted there is removed. Keep this in + # step with docs/static/CNAME. + cname: keepiq.conduction.nl + + docs-hosts: doriath.conduction.nl,keepiq.conduction.nl + + # Named explicitly, because the comment above already knows the answer + # and the workflow did not. The callee derives the worker from `cname` + # when not told — `keepiq-docs`, which does not exist. Deploying that + # creates a SECOND worker while both custom domains keep routing to + # `doriath-docs`: every deploy green, reaching nobody. Renaming the + # worker is a Cloudflare-side move, not something this file can perform. + worker-name: doriath-docs diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 3297b046b..05107ddad 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -14,7 +14,7 @@ jobs: triage: uses: ConductionNL/.github/.github/workflows/issue-triage.yml@main with: - app-name: doriath + app-name: keepiq backlog-existing: ${{ github.event_name == 'workflow_dispatch' && inputs.backlog-existing || false }} secrets: PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }} diff --git a/.github/workflows/openspec-sync.yml b/.github/workflows/openspec-sync.yml index 4283bc11e..ba89b7a98 100644 --- a/.github/workflows/openspec-sync.yml +++ b/.github/workflows/openspec-sync.yml @@ -10,6 +10,6 @@ jobs: sync: uses: ConductionNL/.github/.github/workflows/openspec-sync.yml@main with: - app-name: doriath + app-name: keepiq secrets: PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0782cf632..b32036b0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: uses: ConductionNL/.github/.github/workflows/release.yml@main with: release-type: unstable - app-name: doriath + app-name: keepiq secrets: inherit beta: @@ -41,7 +41,7 @@ jobs: uses: ConductionNL/.github/.github/workflows/release.yml@main with: release-type: beta - app-name: doriath + app-name: keepiq secrets: inherit stable: @@ -49,5 +49,5 @@ jobs: uses: ConductionNL/.github/.github/workflows/release.yml@main with: release-type: stable - app-name: doriath + app-name: keepiq secrets: inherit diff --git a/.gitignore b/.gitignore index fd45ae8b9..45708efad 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,10 @@ openspec/test-site-results/**/*.webp # docs-site build artefacts (regenerated by npm; never track) docs/node_modules/ docs/build/ + +RESTYLE-PLAN.md +# Agent/test scratch and tool caches — generated, never source. +# Added by the 2026-08-25 fleet hygiene sweep (ADR-100 Decision 2). +.stale/ +/.e2e-state/ +.phpunit.result.cache diff --git a/.doriath-csp.mjs b/.keepiq-csp.mjs similarity index 68% rename from .doriath-csp.mjs rename to .keepiq-csp.mjs index c8e98648c..8a3c5841d 100644 --- a/.doriath-csp.mjs +++ b/.keepiq-csp.mjs @@ -2,13 +2,13 @@ import { chromium } from '@playwright/test' const BASE='http://localhost:8088' const b=await chromium.launch(); const c=await b.newContext(); const p=await c.newPage() let csp=null -p.on('response',r=>{ if(r.url().endsWith('/apps/doriath/') || r.url().includes('/apps/doriath/#')) { const h=r.headers(); if(h['content-security-policy']) csp=h['content-security-policy'] } }) +p.on('response',r=>{ if(r.url().endsWith('/apps/keepiq/') || r.url().includes('/apps/keepiq/#')) { const h=r.headers(); if(h['content-security-policy']) csp=h['content-security-policy'] } }) await p.goto(`${BASE}/index.php/login`,{waitUntil:'domcontentloaded'}) await p.locator('input[name="user"]').fill('admin'); await p.locator('input[name="password"]').fill('admin') await p.locator('button[type="submit"]').first().click(); await p.waitForSelector('#header',{timeout:30000}) -const resp = await p.goto(`${BASE}/index.php/apps/doriath/`,{waitUntil:'domcontentloaded'}) +const resp = await p.goto(`${BASE}/index.php/apps/keepiq/`,{waitUntil:'domcontentloaded'}) const h = resp.headers()['content-security-policy'] || '(none)' -console.log('CSP on the doriath SPA page:') +console.log('CSP on the keepiq SPA page:') console.log(h) console.log() console.log("contains 'wasm-unsafe-eval':", h.includes('wasm-unsafe-eval')) diff --git a/.doriath-detail.mjs b/.keepiq-detail.mjs similarity index 85% rename from .doriath-detail.mjs rename to .keepiq-detail.mjs index 1deff923e..986e8994e 100644 --- a/.doriath-detail.mjs +++ b/.keepiq-detail.mjs @@ -6,7 +6,7 @@ p.on('console',m=>{if(m.type()==='error')errs.push('c:'+m.text().slice(0,160))}) await p.goto(`${BASE}/index.php/login`,{waitUntil:'domcontentloaded'}) await p.locator('input[name="user"]').fill('admin'); await p.locator('input[name="password"]').fill('admin') await p.locator('button[type="submit"]').first().click(); await p.waitForSelector('#header',{timeout:30000}) -await p.goto(`${BASE}/index.php/apps/doriath/`,{waitUntil:'domcontentloaded'}); await p.waitForTimeout(3000) +await p.goto(`${BASE}/index.php/apps/keepiq/`,{waitUntil:'domcontentloaded'}); await p.waitForTimeout(3000) await p.locator('.lock-screen input[type="password"]').first().fill('Oj',{force:true}); await p.waitForTimeout(400) await p.evaluate(()=>{const bs=[...document.querySelectorAll('.lock-screen button')];const u=bs.find(b=>/Unlock/i.test(b.textContent||''));if(u)u.click()}) await p.waitForTimeout(4000) @@ -20,8 +20,8 @@ await p.waitForTimeout(3500) console.log('url:',p.url()) const probe = await p.evaluate(()=>({ detail: !!document.querySelector('.secret-detail'), - pwField: document.querySelectorAll('.secret-detail .doriath-password-field').length, - pwInput: document.querySelectorAll('.secret-detail .doriath-password-field input').length, + pwField: document.querySelectorAll('.secret-detail .keepiq-password-field').length, + pwInput: document.querySelectorAll('.secret-detail .keepiq-password-field input').length, anyInput: document.querySelectorAll('.secret-detail input').length, detailText: (document.querySelector('.secret-detail')?.innerText||'').slice(0,200) })) diff --git a/.doriath-href.mjs b/.keepiq-href.mjs similarity index 91% rename from .doriath-href.mjs rename to .keepiq-href.mjs index 596b9edbe..2c60b689d 100644 --- a/.doriath-href.mjs +++ b/.keepiq-href.mjs @@ -6,7 +6,7 @@ await p.locator('input[name="user"]').fill('admin') await p.locator('input[name="password"]').fill('admin') await p.locator('button[type="submit"]').first().click() await p.waitForSelector('#header',{timeout:30000}) -await p.goto(`${BASE}/index.php/apps/doriath/`,{waitUntil:'domcontentloaded'}) +await p.goto(`${BASE}/index.php/apps/keepiq/`,{waitUntil:'domcontentloaded'}) await p.waitForTimeout(4000) const r = await p.evaluate(() => { const nav = document.querySelector('.app-navigation') diff --git a/.doriath-nav.mjs b/.keepiq-nav.mjs similarity index 94% rename from .doriath-nav.mjs rename to .keepiq-nav.mjs index b6b1fbe13..aae29f761 100644 --- a/.doriath-nav.mjs +++ b/.keepiq-nav.mjs @@ -6,7 +6,7 @@ await p.locator('input[name="user"]').fill('admin') await p.locator('input[name="password"]').fill('admin') await p.locator('button[type="submit"]').first().click() await p.waitForSelector('#header',{timeout:30000}) -await p.goto(`${BASE}/index.php/apps/doriath/`,{waitUntil:'domcontentloaded'}) +await p.goto(`${BASE}/index.php/apps/keepiq/`,{waitUntil:'domcontentloaded'}) await p.waitForTimeout(3000) // unlock with the seeded dev master password await p.locator('.lock-screen input[type="password"]').first().fill('Oj',{force:true}) diff --git a/.doriath-neterr.mjs b/.keepiq-neterr.mjs similarity index 81% rename from .doriath-neterr.mjs rename to .keepiq-neterr.mjs index 1fb78e89a..1aa2fc1da 100644 --- a/.doriath-neterr.mjs +++ b/.keepiq-neterr.mjs @@ -9,9 +9,9 @@ await p.locator('input[name="password"]').fill('admin') await p.locator('button[type="submit"]').first().click() await p.waitForSelector('#header', { timeout: 30000 }) bad.length = 0 -await p.goto(`${BASE}/index.php/apps/doriath/`, { waitUntil: 'domcontentloaded' }) +await p.goto(`${BASE}/index.php/apps/keepiq/`, { waitUntil: 'domcontentloaded' }) await p.waitForTimeout(6000) console.log('--- failing requests on app root ---') bad.forEach(x => console.log(x)) -console.log('--- doriath-owned failures:', bad.filter(x => x.includes('/doriath')).length) +console.log('--- keepiq-owned failures:', bad.filter(x => x.includes('/keepiq')).length) await b.close() diff --git a/.doriath-share.mjs b/.keepiq-share.mjs similarity index 89% rename from .doriath-share.mjs rename to .keepiq-share.mjs index 0c32a1d58..5bd229252 100644 --- a/.doriath-share.mjs +++ b/.keepiq-share.mjs @@ -4,11 +4,11 @@ const b=await chromium.launch(); const c=await b.newContext(); const p=await c.n const errs=[]; const reqs=[] p.on('pageerror',e=>errs.push('PE:'+String(e).slice(0,220))) p.on('console',m=>{if(m.type()==='error')errs.push('C:'+m.text().slice(0,220))}) -p.on('response',r=>{if(r.url().includes('/doriath/'))reqs.push(`${r.status()} ${r.request().method()} ${r.url().replace(BASE,'')}`)}) +p.on('response',r=>{if(r.url().includes('/keepiq/'))reqs.push(`${r.status()} ${r.request().method()} ${r.url().replace(BASE,'')}`)}) await p.goto(`${BASE}/index.php/login`,{waitUntil:'domcontentloaded'}) await p.locator('input[name="user"]').fill('admin'); await p.locator('input[name="password"]').fill('admin') await p.locator('button[type="submit"]').first().click(); await p.waitForSelector('#header',{timeout:30000}) -await p.goto(`${BASE}/index.php/apps/doriath/`,{waitUntil:'domcontentloaded'}); await p.waitForTimeout(2500) +await p.goto(`${BASE}/index.php/apps/keepiq/`,{waitUntil:'domcontentloaded'}); await p.waitForTimeout(2500) await p.locator('.lock-screen input[type="password"]').first().fill('Oj',{force:true}); await p.waitForTimeout(400) await p.evaluate(()=>{const bs=[...document.querySelectorAll('.lock-screen button')];const u=bs.find(b=>/Unlock/i.test(b.textContent||''));if(u)u.click()}) await p.waitForTimeout(3500) diff --git a/README.md b/README.md index 37cad54f7..fdff6643c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@

- Doriath logo + Keepiq logo

-

Doriath

+

Keepiq

Encrypted secrets manager for Nextcloud — password manager and key store for users and applications

- Latest release - License - Code quality + Latest release + License + Code quality

--- Securely store and share secrets (passwords, API keys, certificates) for Nextcloud users and applications, using end-to-end RSA/AES encryption backed by a private Certificate Authority. -> **Thick backend architecture** — Doriath owns its own encrypted database tables. No OpenRegister dependency. All secrets are encrypted at rest with RSA-4096 public keys; private keys are AES-256 wrapped with a master password derived key. +> **Thick backend architecture** — Keepiq owns its own encrypted database tables. No OpenRegister dependency. All secrets are encrypted at rest with RSA-4096 public keys; private keys are AES-256 wrapped with a master password derived key. ## Screenshots @@ -72,7 +72,7 @@ _See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full architecture do ### Directory Structure ``` -doriath/ +keepiq/ ├── appinfo/ # Nextcloud app manifest, routes, navigation ├── lib/ # PHP backend │ ├── AppInfo/Application.php @@ -80,7 +80,7 @@ doriath/ │ ├── Service/SettingsService.php │ ├── Listener/DeepLinkRegistrationListener.php │ ├── Repair/InitializeSettings.php -│ └── Settings/ # AdminSettings, doriath_register.json +│ └── Settings/ # AdminSettings, keepiq_register.json ├── templates/ # PHP templates (SPA shells) ├── src/ # Vue 2 frontend │ ├── main.js # App entry point @@ -120,17 +120,17 @@ doriath/ ### From the Nextcloud App Store 1. Go to **Apps** in your Nextcloud instance -2. Search for **Doriath** +2. Search for **Keepiq** 3. Click **Download and enable** ### From Source ```bash cd /var/www/html/custom_apps -git clone https://github.com/ConductionNL/doriath.git doriath -cd doriath +git clone https://github.com/ConductionNL/keepiq.git keepiq +cd keepiq npm install && npm run build -php occ app:enable doriath +php occ app:enable keepiq ``` ## Development @@ -138,7 +138,7 @@ php occ app:enable doriath ### Start the environment Requires a sibling checkout of [openregister](https://github.com/ConductionNL/openregister) -next to this repo (`../openregister`) — Doriath builds on OpenRegister's AppHost engine. +next to this repo (`../openregister`) — Keepiq builds on OpenRegister's AppHost engine. ```bash composer install && npm install && npm run build @@ -146,7 +146,7 @@ docker compose up -d ``` Nextcloud is served at http://localhost:8080 (admin/admin). Both `openregister` -and `doriath` are enabled automatically on every container start by the +and `keepiq` are enabled automatically on every container start by the `before-starting` hook in `docker/nextcloud/enable-apps.sh`. ### Frontend development @@ -179,7 +179,7 @@ The docker compose stack enables both apps automatically on every start. To ```bash npm install && npm run build docker exec nextcloud php occ app:enable openregister -docker exec nextcloud php occ app:enable doriath +docker exec nextcloud php occ app:enable keepiq ``` ## Tech Stack @@ -223,7 +223,7 @@ docker exec nextcloud php occ app:enable doriath ## Related Apps -- **[OpenConnector](https://github.com/ConductionNL/openconnector)** — Uses Doriath as a secret store for connector API credentials +- **[OpenConnector](https://github.com/ConductionNL/openconnector)** — Uses Keepiq as a secret store for connector API credentials _Add related apps here as integrations are built._ diff --git a/Vault-app.docx b/Vault-app.docx deleted file mode 100644 index d760f69e9..000000000 Binary files a/Vault-app.docx and /dev/null differ diff --git a/appinfo/info.xml b/appinfo/info.xml index d69dc0c1c..e0bcfe8a6 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -1,9 +1,9 @@ - doriath - Doriath - Doriath + keepiq + Keepiq + Keepiq Encrypted secrets manager for Nextcloud — password manager and key store for users and applications Versleutelde geheimenbeheerder voor Nextcloud — wachtwoordmanager en sleutelopslag voor gebruikers en applicaties - 0.1.6-unstable.20260819081801 + 0.1.6-unstable.20260826230136 EUPL-1.2 Conduction - Doriath + Keepiq - https://codeberg.org/Conduction/doriath - https://codeberg.org/Conduction/doriath - https://codeberg.org/Conduction/doriath + https://github.com/ConductionNL/keepiq + https://github.com/ConductionNL/keepiq + https://github.com/ConductionNL/keepiq security - https://codeberg.org/Conduction/doriath - https://codeberg.org/Conduction/doriath/issues - https://codeberg.org/Conduction/doriath + https://github.com/ConductionNL/keepiq + https://github.com/ConductionNL/keepiq/issues + https://github.com/ConductionNL/keepiq - https://codeberg.org/Conduction/doriath/raw/branch/main/img/app-store.svg + https://raw.githubusercontent.com/ConductionNL/keepiq/development/img/app-store.svg @@ -105,57 +117,112 @@ Vrij en open source onder de EUPL-1.2-licentie. - OCA\Doriath\BackgroundJob\RenewIntermediateCertificate - OCA\Doriath\BackgroundJob\CheckRootCertificateExpiry - OCA\Doriath\BackgroundJob\PurgeAuditLogJob - OCA\Doriath\BackgroundJob\ExpireSecretRequestsJob - OCA\Doriath\BackgroundJob\ApproveElapsedEmergencyRequests - OCA\Doriath\BackgroundJob\PruneSecretVersionsJob - OCA\Doriath\BackgroundJob\ScanExpiringSecretsJob - OCA\Doriath\BackgroundJob\ExpireMachineLeasesJob - OCA\Doriath\BackgroundJob\EphemeralSendPurgeJob - OCA\Doriath\BackgroundJob\RefreshComplianceMetricsJob - OCA\Doriath\BackgroundJob\DeliverSiemEventsJob - OCA\Doriath\BackgroundJob\ScanCertificateExpiryJob + OCA\Keepiq\BackgroundJob\RenewIntermediateCertificate + OCA\Keepiq\BackgroundJob\CheckRootCertificateExpiry + OCA\Keepiq\BackgroundJob\PurgeAuditLogJob + OCA\Keepiq\BackgroundJob\ExpireSecretRequestsJob + OCA\Keepiq\BackgroundJob\ApproveElapsedEmergencyRequests + OCA\Keepiq\BackgroundJob\PruneSecretVersionsJob + OCA\Keepiq\BackgroundJob\ScanExpiringSecretsJob + OCA\Keepiq\BackgroundJob\ExpireMachineLeasesJob + OCA\Keepiq\BackgroundJob\EphemeralSendPurgeJob + OCA\Keepiq\BackgroundJob\RefreshComplianceMetricsJob + OCA\Keepiq\BackgroundJob\DeliverSiemEventsJob + OCA\Keepiq\BackgroundJob\ScanCertificateExpiryJob + - OCA\Doriath\Repair\BootstrapCertificateAuthority - OCA\Doriath\Repair\InitializeSettings - OCA\Doriath\Repair\SeedSecretTypes - OCA\Doriath\Repair\SeedDevelopmentData - OCA\Doriath\Repair\SeedDevelopmentSecrets - OCA\Doriath\Repair\SeedDevelopmentApplications - OCA\Doriath\Repair\SeedDevelopmentSecretRequests - OCA\Doriath\Repair\SeedDevelopmentLinkShares - OCA\Doriath\Repair\SeedDevelopmentShares - OCA\Doriath\Repair\SeedDevelopmentSecretDelegations + OCA\Keepiq\Repair\MigrateAppConfigKeys + OCA\Keepiq\Repair\MigrateUserPreferences + + OCA\Keepiq\Repair\MigrateSchemaApplicationId + OCA\Keepiq\Repair\BootstrapCertificateAuthority + OCA\Keepiq\Repair\InitializeSettings + OCA\Keepiq\Repair\SeedSecretTypes + OCA\Keepiq\Repair\SeedDevelopmentData + OCA\Keepiq\Repair\SeedDevelopmentSecrets + OCA\Keepiq\Repair\SeedDevelopmentApplications + OCA\Keepiq\Repair\SeedDevelopmentSecretRequests + OCA\Keepiq\Repair\SeedDevelopmentLinkShares + OCA\Keepiq\Repair\SeedDevelopmentShares + OCA\Keepiq\Repair\SeedDevelopmentSecretDelegations - OCA\Doriath\Repair\BootstrapCertificateAuthority - OCA\Doriath\Repair\InitializeSettings - OCA\Doriath\Repair\SeedSecretTypes - OCA\Doriath\Repair\SeedDevelopmentData - OCA\Doriath\Repair\SeedDevelopmentSecrets - OCA\Doriath\Repair\SeedDevelopmentApplications - OCA\Doriath\Repair\SeedDevelopmentSecretRequests - OCA\Doriath\Repair\SeedDevelopmentLinkShares - OCA\Doriath\Repair\SeedDevelopmentShares - OCA\Doriath\Repair\SeedDevelopmentSecretDelegations + OCA\Keepiq\Repair\MigrateAppConfigKeys + OCA\Keepiq\Repair\MigrateUserPreferences + + OCA\Keepiq\Repair\MigrateSchemaApplicationId + OCA\Keepiq\Repair\BootstrapCertificateAuthority + OCA\Keepiq\Repair\InitializeSettings + OCA\Keepiq\Repair\SeedSecretTypes + OCA\Keepiq\Repair\SeedDevelopmentData + OCA\Keepiq\Repair\SeedDevelopmentSecrets + OCA\Keepiq\Repair\SeedDevelopmentApplications + OCA\Keepiq\Repair\SeedDevelopmentSecretRequests + OCA\Keepiq\Repair\SeedDevelopmentLinkShares + OCA\Keepiq\Repair\SeedDevelopmentShares + OCA\Keepiq\Repair\SeedDevelopmentSecretDelegations - OCA\Doriath\Settings\AdminSettings - OCA\Doriath\Sections\SettingsSection + OCA\Keepiq\Settings\AdminSettings + OCA\Keepiq\Sections\SettingsSection - doriath - Doriath - doriath.dashboard.page + keepiq + Keepiq + keepiq.dashboard.page app.svg diff --git a/appinfo/routes.php b/appinfo/routes.php index acc852200..2b3f0ae04 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -3,7 +3,7 @@ declare(strict_types=1); /* - * Doriath route table. + * Keepiq route table. * * The canonical AppHost plumbing routes (dashboard page + SPA catch-all, * settings index/create/load, per-user preferences, and the observability @@ -12,7 +12,7 @@ * /api/metrics URLs are unchanged; their controllers are aliased to the * AppHost generic controllers by Bootstrap::register() in Application.php. * - * Every Doriath domain route is appended via $extra below — it is inserted + * Every Keepiq domain route is appended via $extra below — it is inserted * before the SPA catch-all so it keeps priority over the /{path} fallback. * This file references no OCA\OpenRegister symbol other than the pure array * builder Routes::standard(), so it is safe to require even when OpenRegister @@ -254,6 +254,9 @@ // Machine secret-store API discovery document (public, no auth — // reveals only endpoint shapes; openconnector-secret-store-api §1.1). + // The `doriath` segment survives the doriath -> keepiq rename on purpose: + // it is a published contract URL, not an app id. See the class docblock + // on DiscoveryController for the full reasoning. ['name' => 'discovery#document', 'url' => '/api/v1/app/.well-known/doriath', 'verb' => 'GET'], // JWT-Bearer token exchange (public; signature-verified). diff --git a/browser-extension/README.md b/browser-extension/README.md index 3bb3eb3eb..a336114a4 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -1,7 +1,7 @@ -# Doriath browser extension +# Keepiq browser extension A Manifest V3 WebExtension (Firefox / Chrome / Edge) that brings **autofill**, -**passkey provision**, and **TOTP** to the [Doriath](../) secrets manager — +**passkey provision**, and **TOTP** to the [Keepiq](../) secrets manager — without weakening its zero-knowledge model. The extension is a **second end-to-end client**, exactly the shape ADR-003 @@ -20,7 +20,7 @@ browser-extension/ src/ crypto/ the SAME recipe as the web app (re-exported from ../../src/crypto) lib/ - api.js Doriath API client (pair, match, list, get, create, update) + api.js Keepiq API client (pair, match, list, get, create, update) match.js registrable-domain / origin matching over unencrypted url/name vault.js in-worker unlock/lock state + decrypt-on-demand background/ diff --git a/browser-extension/build.mjs b/browser-extension/build.mjs index 11a8bee5d..a4d123695 100644 --- a/browser-extension/build.mjs +++ b/browser-extension/build.mjs @@ -1,5 +1,5 @@ /** - * Build the Doriath MV3 extension with esbuild. Each entry is bundled to a + * Build the Keepiq MV3 extension with esbuild. Each entry is bundled to a * single self-contained ESM/IIFE file (MV3 forbids remote code + runtime chunk * loading), inlining the shared `src/crypto` and `src/totp` modules verbatim so * the PHP↔JS↔extension crypto stays in lockstep (ADR-003). diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json index 07b25cd31..eeb386ca6 100644 --- a/browser-extension/manifest.json +++ b/browser-extension/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "Doriath", + "name": "Keepiq", "version": "0.1.0", - "description": "Zero-knowledge autofill for the Doriath secrets manager. The vault is unlocked and every secret decrypted inside the extension — the server only ever ships encrypted blobs.", + "description": "Zero-knowledge autofill for the Keepiq secrets manager. The vault is unlocked and every secret decrypted inside the extension — the server only ever ships encrypted blobs.", "permissions": [ "storage", "activeTab", @@ -25,7 +25,7 @@ }, "action": { "default_popup": "popup.html", - "default_title": "Doriath" + "default_title": "Keepiq" }, "content_scripts": [ { diff --git a/browser-extension/src/content/content-script.js b/browser-extension/src/content/content-script.js index 896a1fbde..dbda27467 100644 --- a/browser-extension/src/content/content-script.js +++ b/browser-extension/src/content/content-script.js @@ -204,7 +204,7 @@ function injectShim() { window.addEventListener('message', async (event) => { if (event.source !== window) return const data = event.data - if (!data || data.__doriath !== 'request') return + if (!data || data.__keepiq !== 'request') return const type = data.op === 'create' ? 'webauthn-create' : 'webauthn-get' try { const res = await chrome.runtime.sendMessage({ @@ -213,7 +213,7 @@ window.addEventListener('message', async (event) => { }) window.postMessage( { - __doriath: 'response', + __keepiq: 'response', id: data.id, ...(res || { error: 'no-response' }), }, @@ -221,7 +221,7 @@ window.addEventListener('message', async (event) => { ) } catch (e) { window.postMessage( - { __doriath: 'response', id: data.id, error: e.message || String(e) }, + { __keepiq: 'response', id: data.id, error: e.message || String(e) }, event.origin, ) } diff --git a/browser-extension/src/content/inpage-shim.js b/browser-extension/src/content/inpage-shim.js index 13d0da12f..0662261f2 100644 --- a/browser-extension/src/content/inpage-shim.js +++ b/browser-extension/src/content/inpage-shim.js @@ -23,7 +23,7 @@ window.addEventListener('message', (event) => { if (event.source !== window) return const data = event.data - if (!data || data.__doriath !== 'response') return + if (!data || data.__keepiq !== 'response') return const entry = pending.get(data.id) if (!entry) return pending.delete(data.id) @@ -36,7 +36,7 @@ pending.set(id, resolve) window.postMessage( { - __doriath: 'request', + __keepiq: 'request', id, op, origin: location.origin, diff --git a/browser-extension/src/lib/api.js b/browser-extension/src/lib/api.js index db29cbb0f..88c5eeaeb 100644 --- a/browser-extension/src/lib/api.js +++ b/browser-extension/src/lib/api.js @@ -1,7 +1,7 @@ /** - * Doriath API client for the extension. Authenticates with the paired Nextcloud + * Keepiq API client for the extension. Authenticates with the paired Nextcloud * app-password (HTTP Basic) — never a login password, never a new long-lived - * Doriath secret (browser-extension-autofill §"Pairing"). Every response is an + * Keepiq secret (browser-extension-autofill §"Pairing"). Every response is an * encrypted blob or plaintext index field; the server never returns a decrypted * value. * @@ -10,7 +10,7 @@ * the master password and the derived CryptoKey never touch storage. */ -const CONFIG_KEY = 'doriath.config' +const CONFIG_KEY = 'keepiq.config' /** Load the paired config from storage.local (or null if unpaired). */ export async function loadConfig() { @@ -40,7 +40,7 @@ function base(config) { } async function request(config, method, path, body) { - const res = await fetch(base(config) + '/index.php/apps/doriath' + path, { + const res = await fetch(base(config) + '/index.php/apps/keepiq' + path, { method, headers: { Authorization: authHeader(config), @@ -52,7 +52,7 @@ async function request(config, method, path, body) { }) if (!res.ok) { const text = await res.text().catch(() => '') - const err = new Error(`Doriath ${method} ${path} failed (${res.status})`) + const err = new Error(`Keepiq ${method} ${path} failed (${res.status})`) err.status = res.status err.body = text throw err diff --git a/browser-extension/src/passkey/consent.html b/browser-extension/src/passkey/consent.html index b18b75413..a733cc5e1 100644 --- a/browser-extension/src/passkey/consent.html +++ b/browser-extension/src/passkey/consent.html @@ -2,11 +2,11 @@ - Doriath — passkey consent + Keepiq — passkey consent -
+

Passkey request

diff --git a/browser-extension/src/passkey/consent.js b/browser-extension/src/passkey/consent.js index af733e0ef..101fc97fd 100644 --- a/browser-extension/src/passkey/consent.js +++ b/browser-extension/src/passkey/consent.js @@ -14,8 +14,8 @@ document.getElementById('title').textContent = op === 'create' ? 'Create a passkey?' : 'Sign in with a passkey?' document.getElementById('body').textContent = op === 'create' - ? `Create and store a new passkey for “${rp}” in your Doriath vault?` - : `Use a passkey stored in your Doriath vault to sign in to “${rp}”?` + ? `Create and store a new passkey for “${rp}” in your Keepiq vault?` + : `Use a passkey stored in your Keepiq vault to sign in to “${rp}”?` function respond(allow) { chrome.runtime.sendMessage({ type: 'passkey-consent-result', id, allow }) diff --git a/browser-extension/src/passkey/registration.js b/browser-extension/src/passkey/registration.js index 1fee5e752..13971f0f3 100644 --- a/browser-extension/src/passkey/registration.js +++ b/browser-extension/src/passkey/registration.js @@ -13,7 +13,7 @@ function fallThroughError(message) { // NotAllowedError makes the RP fall back to another authenticator. - return { name: 'NotAllowedError', message: 'Doriath: ' + message } + return { name: 'NotAllowedError', message: 'Keepiq: ' + message } } /** diff --git a/browser-extension/src/popup/popup.css b/browser-extension/src/popup/popup.css index 0225689de..0f7d51ceb 100644 --- a/browser-extension/src/popup/popup.css +++ b/browser-extension/src/popup/popup.css @@ -23,7 +23,7 @@ body { width: 340px; } -.doriath-popup { +.keepiq-popup { padding: 16px; } diff --git a/browser-extension/src/popup/popup.html b/browser-extension/src/popup/popup.html index a1657fe51..08998f07a 100644 --- a/browser-extension/src/popup/popup.html +++ b/browser-extension/src/popup/popup.html @@ -3,14 +3,14 @@ - Doriath + Keepiq -
+