From 3a5cc6629bf5b0ec473c2b95330b3460cbcac813 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 29 May 2026 07:42:54 +0200 Subject: [PATCH 01/12] docs: add Codeberg migration banner --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dbeec34ea..8e0511da2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -

+> [!IMPORTANT] +> ## 🚚 This repository has moved to Codeberg +> +> Active development now happens at **https://codeberg.org/Conduction/openconnector**. +> This GitHub mirror is read-only — issues, pull requests, and new commits should go to Codeberg. +> Update your remote with: `git remote set-url origin https://codeberg.org/Conduction/openconnector`

OpenConnector logo

@@ -280,4 +285,4 @@ EUPL-1.2 ## Authors -Built by [Conduction](https://conduction.nl) -- open-source software for Dutch government and public sector organizations. +Built by [Conduction](https://conduction.nl) -- open-source software for Dutch government and public sector organizations. \ No newline at end of file From 165ab53e0cb789c6d7a274cb50490d5b6d77b72b Mon Sep 17 00:00:00 2001 From: rubenvdlinde Date: Wed, 3 Jun 2026 15:08:53 +0200 Subject: [PATCH 02/12] ci(docs): split build+gated-deploy, short reusable ref, secrets inherit --- .forgejo/workflows/documentation.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/documentation.yml b/.forgejo/workflows/documentation.yml index 5a8898aa5..9623ff3a4 100644 --- a/.forgejo/workflows/documentation.yml +++ b/.forgejo/workflows/documentation.yml @@ -10,12 +10,16 @@ on: - cron: "0 4 * * *" jobs: - docs: - uses: https://codeberg.org/Conduction/.github/.forgejo/workflows/documentation.yml@main + build: + uses: Conduction/.github/.forgejo/workflows/documentation-build.yml@main with: - cf-project-name: openconnector-docs source-folder: docs - secrets: - CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} - CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} - CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }} + secrets: inherit + + deploy: + needs: build + if: github.event_name != 'pull_request' + uses: Conduction/.github/.forgejo/workflows/documentation-deploy.yml@main + with: + cf-project-name: openconnector-docs + secrets: inherit From 67540441990a7bf2853907aacc54e4c21230b4fa Mon Sep 17 00:00:00 2001 From: rubenvdlinde Date: Thu, 4 Jun 2026 23:58:38 +0200 Subject: [PATCH 03/12] ci: drive stable releases via semantic-release [skip ci] --- .forgejo/workflows/release-stable.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.forgejo/workflows/release-stable.yml b/.forgejo/workflows/release-stable.yml index 6695a5f2c..b54d73f3e 100644 --- a/.forgejo/workflows/release-stable.yml +++ b/.forgejo/workflows/release-stable.yml @@ -7,11 +7,7 @@ on: jobs: release: - uses: https://codeberg.org/Conduction/.github/.forgejo/workflows/release-stable.yml@main + uses: Conduction/.github/.forgejo/workflows/release-semrel.yml@main with: app-name: openconnector - secrets: - CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }} - NEXTCLOUD_SIGNING_KEY: ${{ secrets.NEXTCLOUD_SIGNING_KEY }} - NEXTCLOUD_SIGNING_CERT: ${{ secrets.NEXTCLOUD_SIGNING_CERT }} - NEXTCLOUD_APPSTORE_TOKEN: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }} + secrets: inherit From 16bffdb0dda02e5d056c81832b09960a0e6a815b Mon Sep 17 00:00:00 2001 From: rubenvdlinde Date: Mon, 8 Jun 2026 14:17:03 +0200 Subject: [PATCH 04/12] ci(docs): deploy only from documentation branch (decouple from releases) [skip ci] --- .forgejo/workflows/documentation.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.forgejo/workflows/documentation.yml b/.forgejo/workflows/documentation.yml index 9623ff3a4..83a7d7ffa 100644 --- a/.forgejo/workflows/documentation.yml +++ b/.forgejo/workflows/documentation.yml @@ -1,13 +1,13 @@ name: Publish docs +# Docs deploy ONLY from the dedicated `documentation` branch — decoupled from main/development +# so doc edits never trigger releases and code releases never trigger doc builds. No cron. on: push: - branches: [documentation, main, development] + branches: [documentation] pull_request: - branches: [documentation, main] + branches: [documentation] workflow_dispatch: - schedule: - - cron: "0 4 * * *" jobs: build: From ef6723206eb2f3cd7ac517ac5a8d9b917c2cf0eb Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Mon, 8 Jun 2026 16:06:27 +0200 Subject: [PATCH 05/12] fix: PostgreSQL compatibility across mappers and settings OpenConnector contained several MySQL-specific SQL constructs that fail on PostgreSQL (MySQL silently coerces; PostgreSQL errors). These broke job creation, synchronization, log retention and the statistics dashboard on PostgreSQL deployments. - find(): drop `eq('id', $stringId)` from the non-numeric branch in Source, Job, Mapping, Endpoint and Rule mappers. Comparing the bigint `id` column to a uuid/slug threw SQLSTATE[22P02] "invalid input syntax for type bigint". - findByConfiguration() (6 mappers): replace MySQL-only JSON_CONTAINS + backtick table names with a cross-DB QueryBuilder LIKE, matching findByArgumentIds. - RuleMapper::getMaxOrder(): replace COALESCE(MAX(`order`)) raw SQL with func()->max('order'), which quotes the reserved word per platform. - SourcesController: make the slow-requests JSON filter platform-aware (Postgres ::jsonb->>, SQLite json_extract, MySQL JSON_EXTRACT). - SettingsService::rebase(): extract a platform-aware setExpiryDates() helper (Postgres/SQLite/MySQL date arithmetic; CAST(? AS bigint) so PDO's text-bound param multiplies an interval on PG) and a portable columnExists() replacing SHOW COLUMNS; emit empty-string/zero-date predicates only for MySQL. - SettingsService::getStats(): drop backtick-quoted table names so COUNT(*) runs on PostgreSQL instead of silently returning 0. Validated end-to-end against PostgreSQL: job creation, find-by-slug (404 not 500), rebase (success, no errors) and stats (real counts). --- lib/Controller/SourcesController.php | 18 ++- lib/Db/EndpointMapper.php | 13 +- lib/Db/JobMapper.php | 13 +- lib/Db/MappingMapper.php | 13 +- lib/Db/RuleMapper.php | 16 +- lib/Db/SourceMapper.php | 13 +- lib/Db/SynchronizationMapper.php | 10 +- lib/Service/SettingsService.php | 224 ++++++++++++++++++--------- 8 files changed, 219 insertions(+), 101 deletions(-) diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index 297825c9c..ebe47f07a 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -12,6 +12,7 @@ use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; +use OCP\IDBConnection; use OCP\IRequest; class SourcesController extends Controller @@ -28,7 +29,8 @@ public function __construct( IRequest $request, private readonly IAppConfig $config, private readonly SourceMapper $sourceMapper, - private readonly CallLogMapper $callLogMapper + private readonly CallLogMapper $callLogMapper, + private readonly IDBConnection $db ) { parent::__construct($appName, $request); @@ -250,7 +252,19 @@ public function logs(SearchService $searchService): JSONResponse } if (empty($specialFilters['slow_requests']) === false) { - $searchConditions[] = "JSON_EXTRACT(response, '$.responseTime') > ?"; + // The responseTime lives inside the `response` JSON column; JSON access + // syntax differs per database engine, so pick the right expression. + switch ($this->db->getDatabaseProvider()) { + case IDBConnection::PLATFORM_POSTGRES: + $searchConditions[] = "(response::jsonb->>'responseTime')::numeric > ?"; + break; + case IDBConnection::PLATFORM_SQLITE: + $searchConditions[] = "json_extract(response, '$.responseTime') > ?"; + break; + default: // MySQL / MariaDB + $searchConditions[] = "JSON_EXTRACT(response, '$.responseTime') > ?"; + break; + } $searchParams[] = $specialFilters['slow_requests']; } diff --git a/lib/Db/EndpointMapper.php b/lib/Db/EndpointMapper.php index cfc0d3a19..06d0315cc 100644 --- a/lib/Db/EndpointMapper.php +++ b/lib/Db/EndpointMapper.php @@ -45,8 +45,7 @@ public function find(int|string $id): Endpoint $qb->where( $qb->expr()->orX( $qb->expr()->eq('uuid', $qb->createNamedParameter($id)), - $qb->expr()->eq('slug', $qb->createNamedParameter($id)), - $qb->expr()->eq('id', $qb->createNamedParameter($id)) + $qb->expr()->eq('slug', $qb->createNamedParameter($id)) ) ); } else { @@ -285,8 +284,14 @@ public function findByPathRegex(string $path, string $method): array */ public function findByConfiguration(string $configurationId): array { - $sql = 'SELECT * FROM `' . $this->getTableName() . '` WHERE JSON_CONTAINS(configurations, ?)'; - return $this->findEntities($sql, [$configurationId]); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->like('configurations', $qb->createNamedParameter('%"' . $configurationId . '"%')) + ); + + return $this->findEntities(query: $qb); } /** diff --git a/lib/Db/JobMapper.php b/lib/Db/JobMapper.php index 2fc03c321..322a0e6b6 100644 --- a/lib/Db/JobMapper.php +++ b/lib/Db/JobMapper.php @@ -37,8 +37,7 @@ public function find(int|string $id): Job $qb->where( $qb->expr()->orX( $qb->expr()->eq('uuid', $qb->createNamedParameter($id)), - $qb->expr()->eq('slug', $qb->createNamedParameter($id)), - $qb->expr()->eq('id', $qb->createNamedParameter($id)) + $qb->expr()->eq('slug', $qb->createNamedParameter($id)) ) ); } else { @@ -215,8 +214,14 @@ public function getTotalCount(array $filters = []): int */ public function findByConfiguration(string $configurationId): array { - $sql = 'SELECT * FROM `' . $this->getTableName() . '` WHERE JSON_CONTAINS(configurations, ?)'; - return $this->findEntities($sql, [$configurationId]); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->like('configurations', $qb->createNamedParameter('%"' . $configurationId . '"%')) + ); + + return $this->findEntities(query: $qb); } /** diff --git a/lib/Db/MappingMapper.php b/lib/Db/MappingMapper.php index 9cd554f0f..415b4efb5 100644 --- a/lib/Db/MappingMapper.php +++ b/lib/Db/MappingMapper.php @@ -37,8 +37,7 @@ public function find(int|string $id): Mapping $qb->where( $qb->expr()->orX( $qb->expr()->eq('uuid', $qb->createNamedParameter($id)), - $qb->expr()->eq('slug', $qb->createNamedParameter($id)), - $qb->expr()->eq('id', $qb->createNamedParameter($id)) + $qb->expr()->eq('slug', $qb->createNamedParameter($id)) ) ); } else { @@ -199,8 +198,14 @@ public function getTotalCallCount(): int */ public function findByConfiguration(string $configurationId): array { - $sql = 'SELECT * FROM `' . $this->getTableName() . '` WHERE JSON_CONTAINS(configurations, ?)'; - return $this->findEntities($sql, [$configurationId]); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->like('configurations', $qb->createNamedParameter('%"' . $configurationId . '"%')) + ); + + return $this->findEntities(query: $qb); } /** diff --git a/lib/Db/RuleMapper.php b/lib/Db/RuleMapper.php index 50c00a5e4..cedaf30d2 100644 --- a/lib/Db/RuleMapper.php +++ b/lib/Db/RuleMapper.php @@ -46,8 +46,7 @@ public function find(int|string $id): Rule $qb->where( $qb->expr()->orX( $qb->expr()->eq('uuid', $qb->createNamedParameter($id)), - $qb->expr()->eq('slug', $qb->createNamedParameter($id)), - $qb->expr()->eq('id', $qb->createNamedParameter($id)) + $qb->expr()->eq('slug', $qb->createNamedParameter($id)) ) ); } else { @@ -215,13 +214,14 @@ public function updateFromArray(int $id, array $object): Rule private function getMaxOrder(): int { $qb = $this->db->getQueryBuilder(); - $qb->select($qb->createFunction('COALESCE(MAX(`order`), 0) as max_order')) + $qb->selectAlias($qb->func()->max('order'), 'max_order') ->from('openconnector_rules'); $result = $qb->executeQuery(); $row = $result->fetch(); $result->closeCursor(); + // Cast handles a NULL result (no rows) as 0, so COALESCE is unnecessary. return (int)($row['max_order']); } @@ -268,8 +268,14 @@ public function reorder(array $orderMap): void */ public function findByConfiguration(string $configurationId): array { - $sql = 'SELECT * FROM `' . $this->getTableName() . '` WHERE JSON_CONTAINS(configurations, ?)'; - return $this->findEntities($sql, [$configurationId]); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->like('configurations', $qb->createNamedParameter('%"' . $configurationId . '"%')) + ); + + return $this->findEntities(query: $qb); } /** diff --git a/lib/Db/SourceMapper.php b/lib/Db/SourceMapper.php index cc65c9f52..18dc9caf9 100644 --- a/lib/Db/SourceMapper.php +++ b/lib/Db/SourceMapper.php @@ -37,8 +37,7 @@ public function find(int|string $id): Source $qb->where( $qb->expr()->orX( $qb->expr()->eq('uuid', $qb->createNamedParameter($id)), - $qb->expr()->eq('slug', $qb->createNamedParameter($id)), - $qb->expr()->eq('id', $qb->createNamedParameter($id)) + $qb->expr()->eq('slug', $qb->createNamedParameter($id)) ) ); } else { @@ -227,8 +226,14 @@ public function findOrCreateByLocation(string $location, array $defaultData = [] */ public function findByConfiguration(string $configurationId): array { - $sql = 'SELECT * FROM `' . $this->getTableName() . '` WHERE JSON_CONTAINS(configurations, ?)'; - return $this->findEntities($sql, [$configurationId]); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->like('configurations', $qb->createNamedParameter('%"' . $configurationId . '"%')) + ); + + return $this->findEntities(query: $qb); } /** diff --git a/lib/Db/SynchronizationMapper.php b/lib/Db/SynchronizationMapper.php index d24e82761..47a0c7805 100644 --- a/lib/Db/SynchronizationMapper.php +++ b/lib/Db/SynchronizationMapper.php @@ -368,8 +368,14 @@ public function getTotalCallCount(): int */ public function findByConfiguration(string $configurationId): array { - $sql = 'SELECT * FROM `' . $this->getTableName() . '` WHERE JSON_CONTAINS(configurations, ?)'; - return $this->findEntities($sql, [$configurationId]); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->like('configurations', $qb->createNamedParameter('%"' . $configurationId . '"%')) + ); + + return $this->findEntities(query: $qb); } /** diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index 9cfefa9d8..31a5590b1 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -118,23 +118,25 @@ public function getStats(): array // **OPTIMIZED QUERIES**: Use direct SQL COUNT queries for maximum performance - // All tables - simple counts (OpenConnector tables don't have size/expires columns like OpenRegister) + // All tables - simple counts (OpenConnector tables don't have size/expires columns like OpenRegister). + // Table names are left unquoted so the count works across MySQL/MariaDB, PostgreSQL and SQLite + // (backtick quoting is MySQL-only and errors on PostgreSQL). $allTables = [ - 'callLogs' => '`*PREFIX*openconnector_call_logs`', - 'consumers' => '`*PREFIX*openconnector_consumers`', - 'endpoints' => '`*PREFIX*openconnector_endpoints`', - 'eventMessages' => '`*PREFIX*openconnector_event_messages`', - 'eventSubscriptions' => '`*PREFIX*openconnector_event_subscriptions`', - 'events' => '`*PREFIX*openconnector_events`', - 'jobLogs' => '`*PREFIX*openconnector_job_logs`', - 'jobs' => '`*PREFIX*openconnector_jobs`', - 'mappings' => '`*PREFIX*openconnector_mappings`', - 'rules' => '`*PREFIX*openconnector_rules`', - 'sources' => '`*PREFIX*openconnector_sources`', - 'synchronizationContractLogs' => '`*PREFIX*openconnector_synchronization_contract_logs`', - 'synchronizationContracts' => '`*PREFIX*openconnector_synchronization_contracts`', - 'synchronizationLogs' => '`*PREFIX*openconnector_synchronization_logs`', - 'synchronizations' => '`*PREFIX*openconnector_synchronizations`', + 'callLogs' => '*PREFIX*openconnector_call_logs', + 'consumers' => '*PREFIX*openconnector_consumers', + 'endpoints' => '*PREFIX*openconnector_endpoints', + 'eventMessages' => '*PREFIX*openconnector_event_messages', + 'eventSubscriptions' => '*PREFIX*openconnector_event_subscriptions', + 'events' => '*PREFIX*openconnector_events', + 'jobLogs' => '*PREFIX*openconnector_job_logs', + 'jobs' => '*PREFIX*openconnector_jobs', + 'mappings' => '*PREFIX*openconnector_mappings', + 'rules' => '*PREFIX*openconnector_rules', + 'sources' => '*PREFIX*openconnector_sources', + 'synchronizationContractLogs' => '*PREFIX*openconnector_synchronization_contract_logs', + 'synchronizationContracts' => '*PREFIX*openconnector_synchronization_contracts', + 'synchronizationLogs' => '*PREFIX*openconnector_synchronization_logs', + 'synchronizations' => '*PREFIX*openconnector_synchronizations', ]; foreach ($allTables as $key => $tableName) { @@ -285,15 +287,10 @@ public function rebase(): array // 0. Update successful logs expiry dates if (isset($retention['successLogRetention']) === true && $retention['successLogRetention'] > 0) { try { - $retentionMs = $retention['successLogRetention']; - $expiryQuery = " - UPDATE `*PREFIX*openconnector_call_logs` - SET expires = DATE_ADD(created, INTERVAL ? MICROSECOND) - WHERE expires IS NULL OR expires = '' - "; - $stmt = $this->db->prepare($expiryQuery); - $stmt->execute([$retentionMs * 1000]); // Convert ms to microseconds - $results['retentionResults']['callLogsUpdated'] = $stmt->rowCount(); + $results['retentionResults']['callLogsUpdated'] = $this->setExpiryDates( + 'openconnector_call_logs', + (int) $retention['successLogRetention'] + ); } catch (\Exception $e) { $error = 'Failed to set call logs expiry dates: '.$e->getMessage(); $results['errors'][] = $error; @@ -304,15 +301,10 @@ public function rebase(): array // 1. Update call logs expiry dates if (isset($retention['callLogRetention']) && $retention['callLogRetention'] > 0) { try { - $retentionMs = $retention['callLogRetention']; - $expiryQuery = " - UPDATE `*PREFIX*openconnector_call_logs` - SET expires = DATE_ADD(created, INTERVAL ? MICROSECOND) - WHERE expires IS NULL OR expires = '' - "; - $stmt = $this->db->prepare($expiryQuery); - $stmt->execute([$retentionMs * 1000]); // Convert ms to microseconds - $results['retentionResults']['callLogsUpdated'] = $stmt->rowCount(); + $results['retentionResults']['callLogsUpdated'] = $this->setExpiryDates( + 'openconnector_call_logs', + (int) $retention['callLogRetention'] + ); } catch (\Exception $e) { $error = 'Failed to set call logs expiry dates: '.$e->getMessage(); $results['errors'][] = $error; @@ -323,19 +315,11 @@ public function rebase(): array // 2. Update event messages expiry dates (skip if expires column doesn't exist) if (isset($retention['eventMessageRetention']) && $retention['eventMessageRetention'] > 0) { try { - $retentionMs = $retention['eventMessageRetention']; - // Check if expires column exists before updating - $checkQuery = "SHOW COLUMNS FROM `*PREFIX*openconnector_event_messages` LIKE 'expires'"; - $checkResult = $this->db->executeQuery($checkQuery); - if ($checkResult->fetchColumn() !== false) { - $expiryQuery = " - UPDATE `*PREFIX*openconnector_event_messages` - SET expires = DATE_ADD(created, INTERVAL ? MICROSECOND) - WHERE expires IS NULL OR expires = '' - "; - $stmt = $this->db->prepare($expiryQuery); - $stmt->execute([$retentionMs * 1000]); - $results['retentionResults']['eventMessagesUpdated'] = $stmt->rowCount(); + if ($this->columnExists('openconnector_event_messages', 'expires') === true) { + $results['retentionResults']['eventMessagesUpdated'] = $this->setExpiryDates( + 'openconnector_event_messages', + (int) $retention['eventMessageRetention'] + ); } else { $results['retentionResults']['eventMessagesUpdated'] = 'Column expires not found - skipped'; } @@ -349,15 +333,10 @@ public function rebase(): array // 3. Update job logs expiry dates if (isset($retention['jobLogRetention']) && $retention['jobLogRetention'] > 0) { try { - $retentionMs = $retention['jobLogRetention']; - $expiryQuery = " - UPDATE `*PREFIX*openconnector_job_logs` - SET expires = DATE_ADD(created, INTERVAL ? MICROSECOND) - WHERE expires IS NULL OR expires = '' - "; - $stmt = $this->db->prepare($expiryQuery); - $stmt->execute([$retentionMs * 1000]); - $results['retentionResults']['jobLogsUpdated'] = $stmt->rowCount(); + $results['retentionResults']['jobLogsUpdated'] = $this->setExpiryDates( + 'openconnector_job_logs', + (int) $retention['jobLogRetention'] + ); } catch (\Exception $e) { $error = 'Failed to set job logs expiry dates: '.$e->getMessage(); $results['errors'][] = $error; @@ -365,18 +344,15 @@ public function rebase(): array } } - // 4. Update synchronization contract logs expiry dates (handle empty expires values) + // 4. Update synchronization contract logs expiry dates (handle empty/missing created values) if (isset($retention['syncContractLogRetention']) && $retention['syncContractLogRetention'] > 0) { try { - $retentionMs = $retention['syncContractLogRetention']; - $expiryQuery = " - UPDATE `*PREFIX*openconnector_synchronization_contract_logs` - SET expires = DATE_ADD(COALESCE(created, NOW()), INTERVAL ? MICROSECOND) - WHERE expires IS NULL OR expires = '' OR expires = '0000-00-00 00:00:00' OR created IS NOT NULL - "; - $stmt = $this->db->prepare($expiryQuery); - $stmt->execute([$retentionMs * 1000]); - $results['retentionResults']['syncContractLogsUpdated'] = $stmt->rowCount(); + $results['retentionResults']['syncContractLogsUpdated'] = $this->setExpiryDates( + 'openconnector_synchronization_contract_logs', + (int) $retention['syncContractLogRetention'], + coalesceCreatedWithNow: true, + rebaseExisting: true + ); } catch (\Exception $e) { $error = 'Failed to set sync contract logs expiry dates: '.$e->getMessage(); $results['errors'][] = $error; @@ -384,18 +360,15 @@ public function rebase(): array } } - // 5. Update synchronization logs expiry dates (handle empty expires values) + // 5. Update synchronization logs expiry dates (handle empty/missing created values) if (isset($retention['syncLogRetention']) && $retention['syncLogRetention'] > 0) { try { - $retentionMs = $retention['syncLogRetention']; - $expiryQuery = " - UPDATE `*PREFIX*openconnector_synchronization_logs` - SET expires = DATE_ADD(COALESCE(created, NOW()), INTERVAL ? MICROSECOND) - WHERE expires IS NULL OR expires = '' OR expires = '0000-00-00 00:00:00' OR created IS NOT NULL - "; - $stmt = $this->db->prepare($expiryQuery); - $stmt->execute([$retentionMs * 1000]); - $results['retentionResults']['syncLogsUpdated'] = $stmt->rowCount(); + $results['retentionResults']['syncLogsUpdated'] = $this->setExpiryDates( + 'openconnector_synchronization_logs', + (int) $retention['syncLogRetention'], + coalesceCreatedWithNow: true, + rebaseExisting: true + ); } catch (\Exception $e) { $error = 'Failed to set sync logs expiry dates: '.$e->getMessage(); $results['errors'][] = $error; @@ -426,4 +399,103 @@ public function rebase(): array }//end rebase() + /** + * Set expiry dates on a log table in a database-agnostic way. + * + * Computes `expires = + retention` for rows that still need an expiry, + * using the correct date-arithmetic syntax for the active database platform + * (MySQL/MariaDB, PostgreSQL or SQLite). Replaces the previous MySQL-only + * DATE_ADD/backtick query so the rebase works on PostgreSQL too. + * + * @param string $table Unprefixed table name. + * @param int $retentionMs Retention period in milliseconds. + * @param bool $coalesceCreatedWithNow Fall back to the current time when `created` is null. + * @param bool $rebaseExisting Also (re)set rows that already have a `created` value. + * + * @return int Number of affected rows. + * + * @throws \OCP\DB\Exception On query failure. + */ + private function setExpiryDates( + string $table, + int $retentionMs, + bool $coalesceCreatedWithNow = false, + bool $rebaseExisting = false + ): int { + $provider = $this->db->getDatabaseProvider(); + $micros = $retentionMs * 1000; + + // PostgreSQL and SQLite cannot compare a timestamp column to '' or a zero-date, + // so those legacy MySQL conditions are only added for the MySQL family. + $isMysqlFamily = in_array( + $provider, + [IDBConnection::PLATFORM_POSTGRES, IDBConnection::PLATFORM_SQLITE], + true + ) === false; + + // Current-time expression for tables whose `created` may be null. + $now = $provider === IDBConnection::PLATFORM_SQLITE ? "datetime('now')" : 'NOW()'; + $base = $coalesceCreatedWithNow === true ? "COALESCE(created, $now)" : 'created'; + + // Platform-specific "base + retention" date arithmetic. The retention value is + // always bound as a single parameter; only the surrounding syntax differs. + switch ($provider) { + case IDBConnection::PLATFORM_POSTGRES: + // Cast the bound parameter so PostgreSQL multiplies a number, not text. + $expiresExpr = "$base + (CAST(? AS bigint) * INTERVAL '1 microsecond')"; + break; + case IDBConnection::PLATFORM_SQLITE: + $expiresExpr = "datetime($base, '+' || (? / 1000000.0) || ' seconds')"; + break; + default: // MySQL / MariaDB + $expiresExpr = "DATE_ADD($base, INTERVAL ? MICROSECOND)"; + break; + } + + // `expires IS NULL` is portable; the empty-string and zero-date checks are MySQL-only. + $conditions = ['expires IS NULL']; + if ($isMysqlFamily === true) { + $conditions[] = "expires = ''"; + $conditions[] = "expires = '0000-00-00 00:00:00'"; + } + if ($rebaseExisting === true) { + $conditions[] = 'created IS NOT NULL'; + } + + $sql = 'UPDATE *PREFIX*'.$table.' SET expires = '.$expiresExpr.' WHERE '.implode(' OR ', $conditions); + $stmt = $this->db->prepare($sql); + $stmt->execute([$micros]); + + return $stmt->rowCount(); + + }//end setExpiryDates() + + + /** + * Check whether a column exists on a table, portably across database platforms. + * + * Runs a guarded `SELECT ... LIMIT 1`; if the column is absent the query + * throws and we report it as missing. This replaces the MySQL-only `SHOW COLUMNS`. + * + * @param string $table Unprefixed table name. + * @param string $column Column name to check. + * + * @return bool True when the column exists. + */ + private function columnExists(string $table, string $column): bool + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->select($column) + ->from($table) + ->setMaxResults(1); + $qb->executeQuery()->closeCursor(); + return true; + } catch (\Exception $e) { + return false; + } + + }//end columnExists() + + }//end class From aa852a6a1da3fff3a759bee8079c20dd21e536a6 Mon Sep 17 00:00:00 2001 From: rubenvdlinde Date: Tue, 9 Jun 2026 09:03:55 +0200 Subject: [PATCH 06/12] fix: add active-development store notice (not for production before 12 June 2026) + conduction.nl app link --- appinfo/info.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index fc4e83792..a680d9a82 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -4,7 +4,9 @@ openconnector Open Connector Gateway and Service bus functionality - ⚠️ **Active development — not for production use yet.** This app is under active development. Although it may carry a stable release status, **please do not use it in production environments before 12 June 2026.** See the [app page on conduction.nl](https://conduction.nl/apps/openconnector) for release planning and what this app does. + +📰 Bringing Gateway and Service bus functionality to nextcloud The OpenConnector Nextcloud app provides a ESB-framework to work together in an (open) data ecosystem From 55d727eb103c53c58eac18a136e9dd9d70bf748a Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 12 Jun 2026 06:14:07 +0200 Subject: [PATCH 07/12] docs(info): bump app-store dev-notice date to 17 June 2026 + patch bump --- appinfo/info.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index a680d9a82..664a4b892 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -4,7 +4,7 @@ openconnector Open Connector Gateway and Service bus functionality - ⚠️ **Active development — not for production use yet.** This app is under active development. Although it may carry a stable release status, **please do not use it in production environments before 12 June 2026.** See the [app page on conduction.nl](https://conduction.nl/apps/openconnector) for release planning and what this app does. + ⚠️ **Active development — not for production use yet.** This app is under active development. Although it may carry a stable release status, **please do not use it in production environments before 17 June 2026.** See the [app page on conduction.nl](https://conduction.nl/apps/openconnector) for release planning and what this app does. 📰 Bringing Gateway and Service bus functionality to nextcloud @@ -15,7 +15,7 @@ The OpenConnector Nextcloud app provides a ESB-framework to work together in an - 🆓 Map and translate API calls ]]> - 0.2.13 + 0.2.14 agpl integration Conduction From 53f0b07857bca6aefd4e2de18960528612991ae8 Mon Sep 17 00:00:00 2001 From: rubenvdlinde Date: Wed, 24 Jun 2026 10:50:52 +0200 Subject: [PATCH 08/12] ci(forgejo): use short reusable-workflow uses form so Forgejo runs the workflow --- .forgejo/workflows/release-beta.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/release-beta.yml b/.forgejo/workflows/release-beta.yml index e5a6f14d0..e89357c4f 100644 --- a/.forgejo/workflows/release-beta.yml +++ b/.forgejo/workflows/release-beta.yml @@ -7,7 +7,7 @@ on: jobs: release: - uses: https://codeberg.org/Conduction/.github/.forgejo/workflows/release-beta.yml@main + uses: Conduction/.github/.forgejo/workflows/release-beta.yml@main with: app-name: openconnector secrets: From 867dcf2a459bdea5e1158a22b3a1eab1700d7264 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 9 Aug 2026 16:11:54 +0200 Subject: [PATCH 09/12] =?UTF-8?q?ci(main):=20restore=20the=20missing=20pus?= =?UTF-8?q?h=20trigger=20=E2=80=94=20main=20built=20NOTHING=20(#1199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `code-quality.yml` on `main` had no `push:` trigger, only `pull_request`. So a push to the release branch ran no jobs at all. Measured 2026-08-09: openconnector `main` has ZERO Code Quality runs on record. Not failing — absent. A branch with no runs shows no red, which is exactly why this went unseen while every other measurement in the CI programme was made on `development`. `development` has carried the correct trigger for some time: push: branches: [main, development, feature/**, bugfix/**, hotfix/**] It never reached `main` because the corrected workflow travels the release train (development -> beta -> main) and no release has carried it across. Deliberately workflow-only. This does not fix whatever `main` may be failing — it makes `main`'s state VISIBLE, which has to come first. Expect the first run to be red; that red is information the branch has not produced in months. See ConductionNL/.github#285. Co-authored-by: Conduction Release Bot --- .github/workflows/code-quality.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 2c4a305ee..404bae3de 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,8 +1,20 @@ name: Code Quality on: + # `push` on main is the load-bearing one, and it was MISSING. Without it a + # push to the release branch built nothing at all — not red, not green, + # simply no run. Measured 2026-08-09: openconnector `main` had ZERO Code + # Quality runs on record, so nobody could say what state the branch users + # install was in. A branch with no runs shows no red. + # + # `development` has carried this trigger for some time; it never reached + # `main` because the corrected workflow travels the release train and no + # release has carried it across. See ConductionNL/.github#285. + push: + branches: [main, development, feature/**, bugfix/**, hotfix/**] pull_request: branches: [main, master, development] + workflow_dispatch: concurrency: group: quality-${{ github.head_ref || github.ref }} From a07633da408c78477be2cf4689010c6eda89d91c Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Thu, 20 Aug 2026 15:01:18 +0200 Subject: [PATCH 10/12] fix: repair deleteInvalidObjects for OpenRegister magic tables Deleting orphaned register/schema objects during sync cleanup was silently broken in two ways after OpenRegister's move to per-schema magic tables: - updateTargetOpenRegister() called the removed ObjectService::delete() method, fatally crashing the delete path whenever it actually ran. - findAllBySynchronizationAndSchema() joined against the retired, now-permanently-empty openregister_objects table, so it never found any contracts to clean up in the first place, regardless of what was orphaned. Switch to the current ObjectService::deleteObject() API, join against the schema's actual magic table (OCA\OpenRegister\Db\MagicMapper:: TABLE_PREFIX + registerId + schemaId), and stop crashing the whole cleanup loop when a single delete is blocked by a referential integrity constraint or the contract has already vanished. Co-Authored-By: Claude Sonnet 5 --- lib/Db/SynchronizationContractMapper.php | 32 ++++++++++++++++-------- lib/Service/SynchronizationService.php | 13 +++++++--- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/Db/SynchronizationContractMapper.php b/lib/Db/SynchronizationContractMapper.php index 360d8e908..ba039a15f 100644 --- a/lib/Db/SynchronizationContractMapper.php +++ b/lib/Db/SynchronizationContractMapper.php @@ -3,6 +3,7 @@ namespace OCA\OpenConnector\Db; use OCA\OpenConnector\Db\SynchronizationContract; +use OCA\OpenRegister\Db\MagicMapper; use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\QBMapper; @@ -185,36 +186,45 @@ public function findByOriginAndTarget(string $originId, string $targetId): Synch } /** - * Find all synchronization contracts by synchronization ID and where target have the given schema id + * Find all synchronization contracts for a synchronization whose target object + * still exists in the register/schema's OpenRegister magic table. * - * @param string $synchronization The synchronization ID + * OpenRegister stores objects in a dynamically created, per-schema table + * (see OCA\OpenRegister\Db\MagicMapper::TABLE_PREFIX) rather than a single + * generic objects table, so the join target is computed from the register + * and schema IDs rather than being a fixed table name. + * + * @param string $synchronizationId The synchronization ID + * @param string $registerId The target register ID + * @param string $schemaId The target schema ID * - * @return array An array of target IDs or an empty array if none found + * @return array An array of contracts, or an empty array if none found (or + * if the schema's magic table does not exist yet) */ - public function findAllBySynchronizationAndSchema(string $synchronizationId, string $schemaId): array + public function findAllBySynchronizationAndSchema(string $synchronizationId, string $registerId, string $schemaId): array { // Create query builder $qb = $this->db->getQueryBuilder(); - // Build select query with synchronization ID and schema filter + $tableName = MagicMapper::TABLE_PREFIX.$registerId.'_'.$schemaId; + + // Build select query, joined against the schema's magic table $qb->select('c.*') ->from('openconnector_synchronization_contracts', 'c') ->innerJoin( 'c', - 'openregister_objects', + $tableName, 'o', - $qb->expr()->eq('c.target_id', 'o.uuid') + $qb->expr()->eq('c.target_id', 'o._uuid') ) ->where( - $qb->expr()->andX( - $qb->expr()->eq('c.synchronization_id', $qb->createNamedParameter($synchronizationId)), - $qb->expr()->eq('o.schema', $qb->createNamedParameter($schemaId)) - ) + $qb->expr()->eq('c.synchronization_id', $qb->createNamedParameter($synchronizationId)) ); try { return $this->findEntities($qb); } catch (\Exception $e) { + // The schema's magic table may not exist yet (e.g. nothing synced so far). return []; } } diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php index 77917806e..e4f2085ee 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -24,6 +24,7 @@ use OCA\OpenConnector\Db\SynchronizationMapper; use OCA\OpenConnector\Service\Helper\FlowToken; use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Exception\ReferentialIntegrityException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Http\JSONResponse; @@ -1150,9 +1151,8 @@ public function deleteInvalidObjects(Synchronization $synchronization, ?array $s switch ($type) { case 'register/schema': - $targetIdsToDelete = []; [$registerId, $schemaId] = explode(separator: '/', string: $synchronization->getTargetId()); - $allContracts = $this->synchronizationContractMapper->findAllBySynchronizationAndSchema(synchronizationId: $synchronization->getId(), schemaId: $schemaId); + $allContracts = $this->synchronizationContractMapper->findAllBySynchronizationAndSchema(synchronizationId: $synchronization->getId(), registerId: $registerId, schemaId: $schemaId); $allContractTargetIds = []; $allContractSourceIds = []; foreach ($allContracts as $contract) { @@ -1187,7 +1187,9 @@ public function deleteInvalidObjects(Synchronization $synchronization, ?array $s $this->synchronizationContractMapper->update($synchronizationContract); $deletedObjectsCount++; } catch (DoesNotExistException $exception) { - // @todo log + $this->logger->info('Skipped deleting invalid object, contract or target object no longer exists', ['targetId' => $targetIdToDelete, 'exception' => $exception->getMessage()]); + } catch (ReferentialIntegrityException $exception) { + $this->logger->warning('Skipped deleting invalid object due to referential integrity constraints', ['targetId' => $targetIdToDelete, 'exception' => $exception->getMessage()]); } } break; @@ -1493,7 +1495,10 @@ private function updateTargetOpenRegister(SynchronizationContract $synchronizati $synchronizationContract->setTargetLastAction($synchronizationContract->getTargetId() ? 'update' : 'create'); break; case 'delete': - $objectService->delete(object: ['id' => $synchronizationContract->getTargetId()]); + $deleted = $objectService->deleteObject(uuid: $synchronizationContract->getTargetId()); + if ($deleted === false) { + $this->logger->warning('OpenRegister reported the object was not deleted', ['targetId' => $synchronizationContract->getTargetId()]); + } $synchronizationContract->setTargetId(null); $synchronizationContract->setTargetLastAction('delete'); break; From 950b48c744b9363c7b509501b480a9dff347d058 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 21 Aug 2026 12:45:19 +0200 Subject: [PATCH 11/12] ci(release): drop five inert legacy release workflows from main (#1331) * ci(release): drop inert beta-release.yaml (triggered on branch 'never') * ci(release): drop inert stable-release.yaml (triggered on branch 'never') * ci(release): drop inert unstable-release.yaml (triggered on branch 'never') * ci(release): drop inert push-beta-to-beta-release.yaml (triggered on branch 'never') * ci(release): drop inert push-development-to-development-release.yaml (triggered on branch 'never') --- .github/workflows/beta-release.yaml | 214 ----------------- .../workflows/push-beta-to-beta-release.yaml | 72 ------ ...sh-development-to-development-release.yaml | 71 ------ .github/workflows/stable-release.yaml | 222 ------------------ .github/workflows/unstable-release.yaml | 215 ----------------- 5 files changed, 794 deletions(-) delete mode 100644 .github/workflows/beta-release.yaml delete mode 100644 .github/workflows/push-beta-to-beta-release.yaml delete mode 100644 .github/workflows/push-development-to-development-release.yaml delete mode 100644 .github/workflows/stable-release.yaml delete mode 100644 .github/workflows/unstable-release.yaml diff --git a/.github/workflows/beta-release.yaml b/.github/workflows/beta-release.yaml deleted file mode 100644 index 367baddac..000000000 --- a/.github/workflows/beta-release.yaml +++ /dev/null @@ -1,214 +0,0 @@ -name: Beta Release - -on: - push: - branches: - # - beta-release - - never - -jobs: - release-management: - runs-on: ubuntu-latest - steps: - - # Step 1: Checkout the repository with full history - - name: Checkout Code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - ssh-key: ${{ secrets.DEPLOY_KEY }} - - # Step 2: Set the app name from repository name - - name: Set app env - run: | - echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - - # Step 3: Calculate the next beta version - # This reads the main version, increments the patch, and adds/increments the beta suffix - - name: Get current version and append beta suffix - id: increment_version - run: | - # Get the stable version from main branch as base - git fetch origin main - main_version=$(git show origin/main:appinfo/info.xml | grep -oP '(?<=)[^<]+' || echo "") - - # Get current version from beta-release branch - current_version=$(grep -oP '(?<=)[^<]+' appinfo/info.xml || echo "") - - # Split main version into parts (e.g., 1.2.5 -> [1, 2, 5]) - IFS='.' read -ra main_version_parts <<< "$main_version" - - # Increment patch version by 1 from main (e.g., 1.2.5 -> 1.2.6) - next_patch=$((main_version_parts[2] + 1)) - - # Extract beta counter from current version if it exists - # If current version is 1.2.6-beta.1, we'll increment to 1.2.6-beta.2 - beta_counter=1 - if [[ $current_version =~ -beta\.([0-9]+)$ ]]; then - # If current patch version matches next patch, increment the counter - current_patch=$(echo $current_version | grep -oP '^[0-9]+\.[0-9]+\.(\d+)' | cut -d. -f3) - if [ "$current_patch" -eq "$next_patch" ]; then - beta_counter=$((BASH_REMATCH[1] + 1)) - fi - fi - - # Build the new beta version string - beta_version="${main_version_parts[0]}.${main_version_parts[1]}.${next_patch}-beta.${beta_counter}" - - # Export version for use in subsequent steps - echo "NEW_VERSION=$beta_version" >> $GITHUB_ENV - echo "new_version=$beta_version" >> $GITHUB_OUTPUT - echo "Main version: $main_version" - echo "Current version: $current_version" - echo "Using beta version: $beta_version" - - # Step 4: Update the version in info.xml - - name: Update version in info.xml - run: | - sed -i "s|.*|${{ env.NEW_VERSION }}|" appinfo/info.xml - - # Step 5: Commit the new version if there are changes - # The [skip ci] prevents triggering the sync workflow again - - name: Commit version update - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - # Only commit if there are actual changes - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - else - git add appinfo/info.xml - git commit -m "Bump beta version to ${{ env.NEW_VERSION }} [skip ci]" - git push - fi - - # Step 6: Prepare signing certificates for Nextcloud app signing - - name: Prepare Signing Certificate and Key - run: | - echo "${{ secrets.NEXTCLOUD_SIGNING_CERT }}" > signing-cert.crt - echo "${{ secrets.NEXTCLOUD_SIGNING_KEY }}" > signing-key.key - - # Step 7: Install Node.js dependencies - - name: Install npm dependencies - uses: actions/setup-node@v3 - with: - node-version: '18.x' - - # Step 8: Set up PHP with required extensions - - name: Set up PHP and install extensions - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - extensions: zip, gd - - # Step 9: Build the app (install dependencies and compile assets) - - run: npm ci - - run: npm run build - # Use production-optimized composer flags for better performance - - run: composer install --no-dev --optimize-autoloader --classmap-authoritative - - # Step 10: Copy files to package directory, excluding development files - # This creates a clean distribution package without test files, config files, etc. - - name: Copy the package files into the package - run: | - mkdir -p package/${{ github.event.repository.name }} - rsync -av --progress \ - --exclude='/package' \ - --exclude='/.git' \ - --exclude='/.github' \ - --exclude='/.cursor' \ - --exclude='/.vscode' \ - --exclude='/node_modules' \ - --exclude='/src' \ - --exclude='/tests' \ - --exclude='/package.json' \ - --exclude='/package-lock.json' \ - --exclude='/composer.json' \ - --exclude='/composer.lock' \ - --exclude='/phpcs.xml' \ - --exclude='/phpmd.xml' \ - --exclude='/psalm.xml' \ - --exclude='/phpunit.xml' \ - --exclude='/.phpunit.cache' \ - --exclude='.phpunit.result.cache' \ - --exclude='/jest.config.js' \ - --exclude='/webpack.config.js' \ - --exclude='/tsconfig.json' \ - --exclude='/.babelrc' \ - --exclude='/.eslintrc.js' \ - --exclude='/.prettierrc' \ - --exclude='/stylelint.config.js' \ - --exclude='/.gitignore' \ - --exclude='/.gitattributes' \ - --exclude='/signing-key.key' \ - --exclude='/signing-cert.crt' \ - ./ package/${{ github.event.repository.name }}/ - - # Step 11: Create compressed tarball archive - - name: Create Tarball - run: | - cd package && tar -czf ../nextcloud-release.tar.gz ${{ github.event.repository.name }} - - # Step 12: Sign the tarball with private key for Nextcloud verification - - name: Sign the TAR.GZ file with OpenSSL - run: | - openssl dgst -sha512 -sign signing-key.key nextcloud-release.tar.gz | openssl base64 -out nextcloud-release.signature - - # Step 13: Upload tarball and signature as GitHub artifact for debugging/reference - - name: Upload tarball as artifact - uses: actions/upload-artifact@v4 - with: - name: nextcloud-release-${{ env.NEW_VERSION }} - path: | - nextcloud-release.tar.gz - nextcloud-release.signature - retention-days: 30 - - # Step 14: Generate git version information - - name: Git Version - id: version - uses: codacy/git-version@2.7.1 - with: - release-branch: beta-release - - # Step 15: Create GitHub release (marked as prerelease) - - name: Upload Beta Release - uses: ncipollo/release-action@v1.12.0 - with: - tag: v${{ env.NEW_VERSION }} - name: Beta Release ${{ env.NEW_VERSION }} - draft: false - prerelease: true - skipIfReleaseExists: true - - # Step 16: Attach tarball to the GitHub release - - name: Attach tarball to GitHub release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: nextcloud-release.tar.gz - asset_name: ${{ env.APP_NAME }}-${{ env.NEW_VERSION }}.tar.gz - tag: v${{ env.NEW_VERSION }} - overwrite: true - - # Step 17: Upload the app to Nextcloud App Store - # nightly: false means this is a regular beta release, not a nightly build - - name: Upload app to Nextcloud appstore - uses: nextcloud-releases/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 - with: - app_name: ${{ env.APP_NAME }} - appstore_token: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }} - download_url: https://github.com/${{ github.repository }}/releases/download/v${{ env.NEW_VERSION }}/${{ env.APP_NAME }}-${{ env.NEW_VERSION }}.tar.gz - app_private_key: ${{ secrets.NEXTCLOUD_SIGNING_KEY }} - nightly: false - - # Step 18: Verify the release contents - # head -50 limits output to first 50 files for readability - - name: Verify release - run: | - echo "App version: ${{ env.NEW_VERSION }}" - echo "Tarball contents:" - tar -tvf nextcloud-release.tar.gz | head -50 - echo "info.xml contents:" - tar -xOf nextcloud-release.tar.gz ${{ env.APP_NAME }}/appinfo/info.xml diff --git a/.github/workflows/push-beta-to-beta-release.yaml b/.github/workflows/push-beta-to-beta-release.yaml deleted file mode 100644 index 7e8241b55..000000000 --- a/.github/workflows/push-beta-to-beta-release.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Sync Beta to Beta-Release - -permissions: - contents: write - actions: write - -on: - push: - branches: - # - beta - - never - -jobs: - sync-to-beta-release: - runs-on: ubuntu-latest - steps: - # Step 1: Checkout the repository with full history - - name: Checkout Code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - ref: ${{ github.sha }} - ssh-key: ${{ secrets.DEPLOY_KEY }} - - # Step 2: Configure Git to use SSH for authentication - - name: Configure Git SSH - run: | - git config --global user.email "action@github.com" - git config --global user.name "GitHub Action" - git remote set-url origin git@github.com:${{ github.repository }}.git - - # Step 3: Sync beta to beta-release while preserving the beta version - - name: Update beta-release branch - run: | - # Fetch all branches from remote - git fetch origin - - # Get the commit message that triggered this workflow - COMMIT_MSG=$(git log -1 --pretty=%B) - - # Store the current beta-release version if the branch exists - # This version will be restored after syncing to maintain version continuity - BETA_RELEASE_VERSION="" - if git show-ref --quiet refs/remotes/origin/beta-release; then - git checkout origin/beta-release - BETA_RELEASE_VERSION=$(grep -oP '(?<=)[^<]+' appinfo/info.xml || echo "") - echo "Found existing beta-release version: $BETA_RELEASE_VERSION" - fi - - # Check if beta-release branch exists - if git show-ref --quiet refs/remotes/origin/beta-release; then - # Branch exists: checkout and reset to latest beta state - git checkout beta-release - git reset --hard origin/beta # Sync all changes from beta - else - # Branch doesn't exist: create new branch from beta - git checkout -b beta-release origin/beta - fi - - # Restore the beta-release version if it existed - # This ensures version increments are preserved across syncs - if [ ! -z "$BETA_RELEASE_VERSION" ]; then - sed -i "s|.*|${BETA_RELEASE_VERSION}|" appinfo/info.xml - git add appinfo/info.xml - git commit -m "${COMMIT_MSG} - - Restored beta-release version to ${BETA_RELEASE_VERSION}" - fi - - # Push to beta-release branch using SSH with force - # Force push is safe here because we control this branch - git push -f origin beta-release diff --git a/.github/workflows/push-development-to-development-release.yaml b/.github/workflows/push-development-to-development-release.yaml deleted file mode 100644 index 6251c8551..000000000 --- a/.github/workflows/push-development-to-development-release.yaml +++ /dev/null @@ -1,71 +0,0 @@ -name: Sync Development to Development-Release - -permissions: - contents: write - -on: - push: - branches: - # - development - - never - -jobs: - sync-to-development-release: - runs-on: ubuntu-latest - steps: - # Step 1: Checkout the repository with full history - - name: Checkout Code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - ref: ${{ github.sha }} - ssh-key: ${{ secrets.DEPLOY_KEY }} - - # Step 2: Configure Git to use SSH for authentication - - name: Configure Git SSH - run: | - git config --global user.email "action@github.com" - git config --global user.name "GitHub Action" - git remote set-url origin git@github.com:${{ github.repository }}.git - - # Step 3: Sync development to development-release while preserving the unstable version - - name: Update development-release branch - run: | - # Fetch all branches from remote - git fetch origin - - # Get the commit message that triggered this workflow - COMMIT_MSG=$(git log -1 --pretty=%B) - - # Store the current development-release version if the branch exists - # This version will be restored after syncing to maintain version continuity - DEV_RELEASE_VERSION="" - if git show-ref --quiet refs/remotes/origin/development-release; then - git checkout origin/development-release - DEV_RELEASE_VERSION=$(grep -oP '(?<=)[^<]+' appinfo/info.xml || echo "") - echo "Found existing development-release version: $DEV_RELEASE_VERSION" - fi - - # Check if development-release branch exists - if git show-ref --quiet refs/remotes/origin/development-release; then - # Branch exists: checkout and reset to latest development state - git checkout development-release - git reset --hard origin/development # Sync all changes from development - else - # Branch doesn't exist: create new branch from development - git checkout -b development-release origin/development - fi - - # Restore the development-release version if it existed - # This ensures version increments are preserved across syncs - if [ ! -z "$DEV_RELEASE_VERSION" ]; then - sed -i "s|.*|${DEV_RELEASE_VERSION}|" appinfo/info.xml - git add appinfo/info.xml - git commit -m "${COMMIT_MSG} - - Restored development-release version to ${DEV_RELEASE_VERSION}" - fi - - # Push to development-release branch using SSH with force - # Force push is safe here because we control this branch - git push -f origin development-release diff --git a/.github/workflows/stable-release.yaml b/.github/workflows/stable-release.yaml deleted file mode 100644 index c91070e44..000000000 --- a/.github/workflows/stable-release.yaml +++ /dev/null @@ -1,222 +0,0 @@ -name: Stable Release - -on: - push: - branches: - # - main - - never - workflow_dispatch: - inputs: - version: - description: 'Version to release (leave empty to use info.xml version)' - required: false - default: '' - -jobs: - release-management: - runs-on: ubuntu-latest - steps: - - - name: Checkout Code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - ssh-key: ${{ secrets.DEPLOY_KEY }} - - - name: Set app env - run: | - echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - - - name: Get current version and increment - id: increment_version - run: | - current_version=$(grep -oP '(?<=)[^<]+' appinfo/info.xml) - IFS='.' read -ra version_parts <<< "$current_version" - ((version_parts[2]++)) - new_version="${version_parts[0]}.${version_parts[1]}.${version_parts[2]}" - echo "NEW_VERSION=$new_version" >> $GITHUB_ENV - echo "new_version=$new_version" >> $GITHUB_OUTPUT - - - name: Update version in info.xml - run: | - sed -i "s|.*|${{ env.NEW_VERSION }}|" appinfo/info.xml - - - name: Commit version update - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git commit -am "Bump version to ${{ env.NEW_VERSION }}" -m "[skip ci]" - git push - - # Step 1: Prepare the signing certificate and key - - name: Prepare Signing Certificate and Key - run: | - echo "${{ secrets.NEXTCLOUD_SIGNING_CERT }}" > signing-cert.crt - echo "${{ secrets.NEXTCLOUD_SIGNING_KEY }}" > signing-key.key - - # Step 3: Install Node.js dependencies using npm - - name: Install npm dependencies - uses: actions/setup-node@v3 - with: - node-version: '18.x' # Specify Node.js version - - # Step 4: Install PHP extensions - - name: Set up PHP and install extensions - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - extensions: zip, gd - - # Step 5: Build the node dependencies - - run: npm ci - - # Step 6: Build the node dependencies - - run: npm run build - - # Step 7: Build composer dependencies - - run: composer i --no-dev - - # Step 8: Copy the files into the package directory - - name: Copy the package files into the package - run: | - mkdir -p package/${{ github.event.repository.name }} - rsync -av --progress \ - --exclude='package' \ - --exclude='.git' \ - --exclude='.github' \ - --exclude='.vscode' \ - --exclude='docker' \ - --exclude='docs' \ - --exclude='website' \ - --exclude='node_modules' \ - --exclude='/src' \ - --exclude='test' \ - --exclude='package-lock.json' \ - --exclude='composer.lock' \ - --exclude='composer-setup.php' \ - --exclude='.phpunit.result.cache' \ - --exclude='phpmd.xml' \ - --exclude='signing-key.key' \ - --exclude='package.json' \ - --exclude='composer.json' \ - --exclude='coverage.txt' \ - --exclude='signing-cert.crt' \ - --exclude='docker-compose.yml' \ - --exclude='webpack.config.js' \ - --exclude='.prettierrc' \ - --exclude='psalm.xml' \ - --exclude='phpunit.xml' \ - --exclude='tsconfig.json' \ - --exclude='changelog-ci-config.json' \ - --exclude='jest.config.js' \ - --exclude='.gitattributes' \ - --exclude='.php-cs-fixer.dist.php' \ - --exclude='.gitignore' \ - --exclude='.eslintrc.js' \ - --exclude='stylelint.config.js' \ - --exclude='.babelrc' \ - --exclude='.nvmrc' \ - ./ package/${{ github.event.repository.name }}/ - - # Step 9: Create the TAR.GZ archive - - name: Create Tarball - run: | - cd package && tar -czf ../nextcloud-release.tar.gz ${{ github.event.repository.name }} - - # Step 10: Sign the TAR.GZ file with OpenSSL - - name: Sign the TAR.GZ file with OpenSSL - run: | - openssl dgst -sha512 -sign signing-key.key nextcloud-release.tar.gz | openssl base64 -out nextcloud-release.signature - - # Step 11: Generate Git version information - - name: Git Version - id: version - uses: codacy/git-version@2.7.1 - with: - release-branch: main - - # Step 12: Extract repository description - - name: Extract repository description - id: repo-description - run: | - description=$(jq -r '.description' <(curl -s https://api.github.com/repos/${{ github.repository }})) - echo "REPO_DESCRIPTION=$description" >> $GITHUB_ENV - - # Step 14: Output the version - - name: Use the version - run: | - echo ${{ steps.version.outputs.version }} - - # Step 15: Copy the package files into the package (this step seems redundant, consider removing) - - name: Copy the package files into the package - run: | - mkdir -p package/${{ github.event.repository.name }} - rsync -av --progress --exclude='package' --exclude='.git' ./ package/${{ github.event.repository.name }}/ - - # Step 18: Create a new release on GitHub - - name: Upload Release - uses: ncipollo/release-action@v1.12.0 - with: - tag: v${{ env.NEW_VERSION }} - name: Release ${{ env.NEW_VERSION }} - draft: false - prerelease: false - - - name: Attach tarball to github release - uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # v2 - id: attach_to_release - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: nextcloud-release.tar.gz # Corrected spelling - asset_name: ${{ env.APP_NAME }}-${{ env.NEW_VERSION }}.tar.gz - tag: v${{ env.NEW_VERSION }} - overwrite: true - - - name: Upload app to Nextcloud appstore - uses: nextcloud-releases/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1 - with: - app_name: ${{ env.APP_NAME }} - appstore_token: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }} - download_url: https://github.com/${{ github.repository }}/releases/download/v${{ env.NEW_VERSION }}/${{ env.APP_NAME }}-${{ env.NEW_VERSION }}.tar.gz - app_private_key: ${{ secrets.NEXTCLOUD_SIGNING_KEY }} - nightly: false - - - name: Verify version and contents - run: | - echo "App version: ${{ env.NEW_VERSION }}" - echo "Tarball contents:" - tar -tvf nextcloud-release.tar.gz - echo "info.xml contents:" - tar -xOf nextcloud-release.tar.gz ${{ env.APP_NAME }}/appinfo/info.xml - - update-changelog: - runs-on: ubuntu-latest - steps: - - - name: Checkout Code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set app env - run: | - echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - - - name: Get current version and increment - id: increment_version - run: | - current_version=$(grep -oP '(?<=)[^<]+' appinfo/info.xml) - IFS='.' read -ra version_parts <<< "$current_version" - ((version_parts[2]++)) - new_version="${version_parts[0]}.${version_parts[1]}.${version_parts[2]}" - echo "NEW_VERSION=$new_version" >> $GITHUB_ENV - echo "new_version=$new_version" >> $GITHUB_OUTPUT - - # Step 13: Run Changelog CI - - name: Run Changelog CI - if: github.ref == 'refs/heads/main' - uses: saadmk11/changelog-ci@v1.1.2 - with: - persist-credentials: true - release_version: ${{ env.NEW_VERSION }} - config_file: changelog-ci-config.json diff --git a/.github/workflows/unstable-release.yaml b/.github/workflows/unstable-release.yaml deleted file mode 100644 index 9d9d1ae37..000000000 --- a/.github/workflows/unstable-release.yaml +++ /dev/null @@ -1,215 +0,0 @@ -name: Unstable Release - -on: - push: - branches: - # - development-release - - never - -jobs: - release-management: - runs-on: ubuntu-latest - steps: - - # Step 1: Checkout the repository with full history - - name: Checkout Code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - ssh-key: ${{ secrets.DEPLOY_KEY }} - - # Step 2: Set the app name from repository name - - name: Set app env - run: | - echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - - # Step 3: Calculate the next unstable version - # This reads the main version, increments the patch, and adds/increments the unstable suffix - - name: Get current version and append unstable suffix - id: increment_version - run: | - # Get the stable version from main branch as base - git fetch origin main - main_version=$(git show origin/main:appinfo/info.xml | grep -oP '(?<=)[^<]+' || echo "") - - # Get current version from development-release branch - current_version=$(grep -oP '(?<=)[^<]+' appinfo/info.xml || echo "") - - # Split main version into parts (e.g., 1.2.5 -> [1, 2, 5]) - IFS='.' read -ra main_version_parts <<< "$main_version" - - # Increment patch version by 1 from main (e.g., 1.2.5 -> 1.2.6) - next_patch=$((main_version_parts[2] + 1)) - - # Extract unstable counter from current version if it exists - # If current version is 1.2.6-unstable.1, we'll increment to 1.2.6-unstable.2 - unstable_counter=1 - if [[ $current_version =~ -unstable\.([0-9]+)$ ]]; then - # If current patch version matches next patch, increment the counter - current_patch=$(echo $current_version | grep -oP '^[0-9]+\.[0-9]+\.(\d+)' | cut -d. -f3) - if [ "$current_patch" -eq "$next_patch" ]; then - unstable_counter=$((BASH_REMATCH[1] + 1)) - fi - fi - - # Build the new unstable version string - unstable_version="${main_version_parts[0]}.${main_version_parts[1]}.${next_patch}-unstable.${unstable_counter}" - - # Export version for use in subsequent steps - echo "NEW_VERSION=$unstable_version" >> $GITHUB_ENV - echo "new_version=$unstable_version" >> $GITHUB_OUTPUT - echo "Main version: $main_version" - echo "Current version: $current_version" - echo "Using unstable version: $unstable_version" - - # Step 4: Update the version in info.xml - - name: Update version in info.xml - run: | - sed -i "s|.*|${{ env.NEW_VERSION }}|" appinfo/info.xml - - # Step 5: Commit the new version if there are changes - # The [skip ci] prevents triggering the sync workflow again - - name: Commit version update - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - # Only commit if there are actual changes - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - else - git add appinfo/info.xml - git commit -m "Bump unstable version to ${{ env.NEW_VERSION }} [skip ci]" - git push - fi - - # Step 6: Prepare signing certificates for Nextcloud app signing - - name: Prepare Signing Certificate and Key - run: | - echo "${{ secrets.NEXTCLOUD_SIGNING_CERT }}" > signing-cert.crt - echo "${{ secrets.NEXTCLOUD_SIGNING_KEY }}" > signing-key.key - - # Step 7: Install Node.js dependencies - - name: Install npm dependencies - uses: actions/setup-node@v3 - with: - node-version: '18.x' - - # Step 8: Set up PHP with required extensions - - name: Set up PHP and install extensions - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - extensions: zip, gd - - # Step 9: Build the app (install dependencies and compile assets) - - run: npm ci - - run: npm run build - # Use production-optimized composer flags for better performance - - run: composer install --no-dev --optimize-autoloader --classmap-authoritative - - # Step 10: Copy files to package directory, excluding development files - # This creates a clean distribution package without test files, config files, etc. - - name: Copy the package files into the package - run: | - mkdir -p package/${{ github.event.repository.name }} - rsync -av --progress \ - --exclude='/package' \ - --exclude='/.git' \ - --exclude='/.github' \ - --exclude='/.cursor' \ - --exclude='/.vscode' \ - --exclude='/node_modules' \ - --exclude='/src' \ - --exclude='/tests' \ - --exclude='/package.json' \ - --exclude='/package-lock.json' \ - --exclude='/composer.json' \ - --exclude='/composer.lock' \ - --exclude='/phpcs.xml' \ - --exclude='/phpmd.xml' \ - --exclude='/psalm.xml' \ - --exclude='/phpunit.xml' \ - --exclude='/.phpunit.cache' \ - --exclude='.phpunit.result.cache' \ - --exclude='/jest.config.js' \ - --exclude='/webpack.config.js' \ - --exclude='/tsconfig.json' \ - --exclude='/.babelrc' \ - --exclude='/.eslintrc.js' \ - --exclude='/.prettierrc' \ - --exclude='/stylelint.config.js' \ - --exclude='/.gitignore' \ - --exclude='/.gitattributes' \ - --exclude='/signing-key.key' \ - --exclude='/signing-cert.crt' \ - ./ package/${{ github.event.repository.name }}/ - - # Step 11: Create compressed tarball archive - - name: Create Tarball - run: | - cd package && tar -czf ../nextcloud-release.tar.gz ${{ github.event.repository.name }} - - # Step 12: Sign the tarball with private key for Nextcloud verification - - name: Sign the TAR.GZ file with OpenSSL - run: | - openssl dgst -sha512 -sign signing-key.key nextcloud-release.tar.gz | openssl base64 -out nextcloud-release.signature - - # Step 13: Upload tarball and signature as GitHub artifact for debugging/reference - - name: Upload tarball as artifact - uses: actions/upload-artifact@v4 - with: - name: nextcloud-release-${{ env.NEW_VERSION }} - path: | - nextcloud-release.tar.gz - nextcloud-release.signature - retention-days: 30 - - # Step 14: Generate git version information - - name: Git Version - id: version - uses: codacy/git-version@2.7.1 - with: - release-branch: development-release - - # Step 15: Create GitHub release (marked as prerelease) - # skipIfReleaseExists prevents errors if the release was already created - - name: Upload Unstable Release - uses: ncipollo/release-action@v1.12.0 - with: - tag: v${{ env.NEW_VERSION }} - name: Unstable Release ${{ env.NEW_VERSION }} - draft: false - prerelease: true - skipIfReleaseExists: true - - # Step 16: Attach tarball to the GitHub release - - name: Attach tarball to GitHub release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: nextcloud-release.tar.gz - asset_name: ${{ env.APP_NAME }}-${{ env.NEW_VERSION }}.tar.gz - tag: v${{ env.NEW_VERSION }} - overwrite: true - - # Step 17: Upload the app to Nextcloud App Store - # nightly: true marks this as a nightly/unstable build in the app store - - name: Upload app to Nextcloud appstore - uses: nextcloud-releases/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 - with: - app_name: ${{ env.APP_NAME }} - appstore_token: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }} - download_url: https://github.com/${{ github.repository }}/releases/download/v${{ env.NEW_VERSION }}/${{ env.APP_NAME }}-${{ env.NEW_VERSION }}.tar.gz - app_private_key: ${{ secrets.NEXTCLOUD_SIGNING_KEY }} - nightly: true - - # Step 18: Verify the release contents - # head -50 limits output to first 50 files for readability - - name: Verify release - run: | - echo "App version: ${{ env.NEW_VERSION }}" - echo "Tarball contents:" - tar -tvf nextcloud-release.tar.gz | head -50 - echo "info.xml contents:" - tar -xOf nextcloud-release.tar.gz ${{ env.APP_NAME }}/appinfo/info.xml From 7e611b7b7a702a39b4fd84ee1f32d060f0d00026 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 30 Aug 2026 08:57:00 +0200 Subject: [PATCH 12/12] chore(deps): aim dependabot at development (#1658) Dependabot reads its config from the DEFAULT branch (main here), whose copy carried no target-branch - so every PR was opened against main where branch-protection rejects it. This is development's file verbatim. --- .github/dependabot.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..1ffe9d367 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,40 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + target-branch: "development" + open-pull-requests-limit: 10 + cooldown: + default-days: 1 + include: + - "*" + exclude: + - "@conduction/*" + + # Composer had NO entry at all, so PHP dependencies were updated with no + # cooldown whatsoever — the window in which a compromised release is still + # published is exactly the window an instant update walks into. `npm` above + # has had one for a while; composer was simply never added, which is not a + # decision anyone made. + # + # Two days rather than one: that is the floor gate-93 enforces, and the npm + # entry's single day predates it. + # + # Our own packages are excluded from the wait on purpose. A cooldown protects + # against a compromised upstream release; `conduction/*` comes from this + # fleet's own CI, and delaying it would only slow the loop between a fix + # being released here and arriving here. + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + target-branch: "development" + open-pull-requests-limit: 10 + cooldown: + default-days: 2 + include: + - "*" + exclude: + - "conduction/*"