From b565a5f24b00eed25712b13c0799b39e96a19896 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 18:43:28 -0300 Subject: [PATCH 1/4] feat: support portable workflow sync authentication --- .../sync-workflow-templates.yml | 101 ++++++++++++++++-- 1 file changed, 94 insertions(+), 7 deletions(-) diff --git a/workflow-templates/sync-workflow-templates.yml b/workflow-templates/sync-workflow-templates.yml index f3b4260..69f1612 100644 --- a/workflow-templates/sync-workflow-templates.yml +++ b/workflow-templates/sync-workflow-templates.yml @@ -6,8 +6,14 @@ # SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: MIT -# This workflow will update all workflow templates -# Additionally it will reapply `workflow.yml.patch` files after syncing and only then commit the result +# This workflow will update all workflow templates. +# Additionally it will reapply workflow.yml.patch files after syncing and only then commit the result. +# +# Authentication is explicit through vars.WORKFLOW_SYNC_AUTH_MODE: +# - librecode-app (default): LibreCode-managed GitHub App credentials +# - github-app: consumer-owned GitHub App credentials +# - token: consumer-owned repository-scoped token +# - github-token: built-in GITHUB_TOKEN (generated PRs do not trigger normal PR workflows) name: Update workflows on: workflow_dispatch: @@ -39,8 +45,48 @@ jobs: with: require: admin - - name: Create GitHub App token - id: app-token + - name: Validate workflow sync authentication + shell: bash + env: + AUTH_MODE: ${{ vars.WORKFLOW_SYNC_AUTH_MODE || 'librecode-app' }} + LIBRECODE_APP_ID: ${{ vars.LIBRECODE_WORKFLOW_APP_ID }} + LIBRECODE_APP_PRIVATE_KEY: ${{ secrets.LIBRECODE_WORKFLOW_APP_PRIVATE_KEY }} + CONSUMER_APP_ID: ${{ vars.WORKFLOW_SYNC_APP_ID }} + CONSUMER_APP_PRIVATE_KEY: ${{ secrets.WORKFLOW_SYNC_APP_PRIVATE_KEY }} + CONSUMER_TOKEN: ${{ secrets.WORKFLOW_SYNC_TOKEN }} + run: | + set -euo pipefail + + case "${AUTH_MODE}" in + librecode-app) + [[ -n "${LIBRECODE_APP_ID}" && -n "${LIBRECODE_APP_PRIVATE_KEY}" ]] || { + echo "::error::librecode-app requires LIBRECODE_WORKFLOW_APP_ID and LIBRECODE_WORKFLOW_APP_PRIVATE_KEY" + exit 1 + } + ;; + github-app) + [[ -n "${CONSUMER_APP_ID}" && -n "${CONSUMER_APP_PRIVATE_KEY}" ]] || { + echo "::error::github-app requires WORKFLOW_SYNC_APP_ID and WORKFLOW_SYNC_APP_PRIVATE_KEY" + exit 1 + } + ;; + token) + [[ -n "${CONSUMER_TOKEN}" ]] || { + echo "::error::token mode requires WORKFLOW_SYNC_TOKEN" + exit 1 + } + ;; + github-token) + ;; + *) + echo "::error::Unsupported WORKFLOW_SYNC_AUTH_MODE: ${AUTH_MODE}" + exit 1 + ;; + esac + + - name: Create LibreCode GitHub App token + if: ${{ vars.WORKFLOW_SYNC_AUTH_MODE == '' || vars.WORKFLOW_SYNC_AUTH_MODE == 'librecode-app' }} + id: librecode-app-token uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 with: app-id: ${{ vars.LIBRECODE_WORKFLOW_APP_ID }} @@ -51,6 +97,47 @@ jobs: permission-pull-requests: write permission-workflows: write + - name: Create consumer GitHub App token + if: ${{ vars.WORKFLOW_SYNC_AUTH_MODE == 'github-app' }} + id: consumer-app-token + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + with: + app-id: ${{ vars.WORKFLOW_SYNC_APP_ID }} + private-key: ${{ secrets.WORKFLOW_SYNC_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: write + permission-pull-requests: write + permission-workflows: write + + - name: Resolve workflow sync token + id: auth-token + shell: bash + env: + AUTH_MODE: ${{ vars.WORKFLOW_SYNC_AUTH_MODE || 'librecode-app' }} + LIBRECODE_APP_TOKEN: ${{ steps.librecode-app-token.outputs.token }} + CONSUMER_APP_TOKEN: ${{ steps.consumer-app-token.outputs.token }} + CONSUMER_TOKEN: ${{ secrets.WORKFLOW_SYNC_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + case "${AUTH_MODE}" in + librecode-app) token="${LIBRECODE_APP_TOKEN}" ;; + github-app) token="${CONSUMER_APP_TOKEN}" ;; + token) token="${CONSUMER_TOKEN}" ;; + github-token) token="${GITHUB_TOKEN}" ;; + *) exit 1 ;; + esac + + [[ -n "${token}" ]] || { + echo "::error::Selected workflow sync authentication produced an empty token" + exit 1 + } + + echo "::add-mask::${token}" + echo "token=${token}" >> "${GITHUB_OUTPUT}" + - name: Checkout workflow repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -76,10 +163,10 @@ jobs: if: ${{ steps.sync.outputs.changed == 'true' }} uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: - token: ${{ steps.app-token.outputs.token }} + token: ${{ steps.auth-token.outputs.token }} commit-message: 'ci(actions): Update workflow templates from organization template repository' committer: GitHub - author: librecode-workflow-automation[bot] <331658022+librecode-workflow-automation[bot]@users.noreply.github.com> + author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> path: target signoff: true branch: 'automated/noid/${{ matrix.branches }}-update-workflows' @@ -87,7 +174,7 @@ jobs: draft: ${{ steps.sync.outputs.patch_failed == 'true' }} add-paths: .github/workflows/*.yml,.github/actions-lock.txt body: | - Automated update of all workflow templates from [LibreCodeCoop/.github](https://github.com/LibreCodeCoop/.github) + Automated update of workflow templates from [LibreCodeCoop/.github](https://github.com/LibreCodeCoop/.github) ${{ steps.sync.outputs.summary }} labels: | dependencies From af565b11cc8266f1f67ae3975327c2ad22ce7d33 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 18:43:53 -0300 Subject: [PATCH 2/4] test: cover portable workflow sync authentication --- tests/test_portable_workflow_sync_auth.py | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/test_portable_workflow_sync_auth.py diff --git a/tests/test_portable_workflow_sync_auth.py b/tests/test_portable_workflow_sync_auth.py new file mode 100644 index 0000000..e6a24c4 --- /dev/null +++ b/tests/test_portable_workflow_sync_auth.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +from pathlib import Path +import unittest + +ROOT = Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / "workflow-templates" / "sync-workflow-templates.yml" + + +class PortableWorkflowSyncAuthTest(unittest.TestCase): + def test_template_supports_explicit_authentication_modes(self) -> None: + content = TEMPLATE.read_text(encoding="utf-8") + + self.assertIn("WORKFLOW_SYNC_AUTH_MODE", content) + self.assertIn("librecode-app", content) + self.assertIn("github-app", content) + self.assertIn("token", content) + self.assertIn("github-token", content) + self.assertIn("WORKFLOW_SYNC_APP_ID", content) + self.assertIn("WORKFLOW_SYNC_APP_PRIVATE_KEY", content) + self.assertIn("WORKFLOW_SYNC_TOKEN", content) + + def test_external_modes_do_not_require_librecode_credentials(self) -> None: + content = TEMPLATE.read_text(encoding="utf-8") + + self.assertIn("Create consumer GitHub App token", content) + self.assertIn("vars.WORKFLOW_SYNC_AUTH_MODE == 'github-app'", content) + self.assertIn('github-app) token="${CONSUMER_APP_TOKEN}"', content) + self.assertIn('token) token="${CONSUMER_TOKEN}"', content) + + def test_generated_pull_request_uses_selected_token(self) -> None: + content = TEMPLATE.read_text(encoding="utf-8") + + self.assertIn("id: auth-token", content) + self.assertIn("token: ${{ steps.auth-token.outputs.token }}", content) + + def test_github_token_limitation_is_visible_in_template(self) -> None: + content = TEMPLATE.read_text(encoding="utf-8") + self.assertIn( + "generated PRs do not trigger normal PR workflows", + content, + ) + + +if __name__ == "__main__": + unittest.main() From 2591cc2a9bb2a18d32eaa7724ff415eeb6789643 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 18:43:56 -0300 Subject: [PATCH 3/4] docs: document portable workflow sync authentication --- docs/cross-repository-automation.md | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/cross-repository-automation.md b/docs/cross-repository-automation.md index b3da5d1..3d94fbc 100644 --- a/docs/cross-repository-automation.md +++ b/docs/cross-repository-automation.md @@ -112,3 +112,35 @@ The initial production validation confirmed: - `LibreCodeCoop/extract` can be checked out and updated; - managed workflows can be adopted into the lock file; - a subsequent synchronization with current hashes creates no PR. + + +## Portable consumer authentication + +The installed workflow updater does not infer authentication from the consumer's +organization name. Consumers select an explicit repository variable: + +\`WORKFLOW_SYNC_AUTH_MODE\` + +Supported values: + +- \`librecode-app\` — default for existing LibreCode-managed repositories. Uses + \`LIBRECODE_WORKFLOW_APP_ID\` and \`LIBRECODE_WORKFLOW_APP_PRIVATE_KEY\`. +- \`github-app\` — uses a consumer-owned GitHub App configured through + \`WORKFLOW_SYNC_APP_ID\` and \`WORKFLOW_SYNC_APP_PRIVATE_KEY\`. +- \`token\` — uses a consumer-owned repository-scoped credential stored as + \`WORKFLOW_SYNC_TOKEN\`. +- \`github-token\` — uses the workflow's built-in \`GITHUB_TOKEN\`. + +A consumer-owned GitHub App is preferred for independent projects because it +keeps credentials under the consumer's control while still allowing generated +pull requests to trigger normal repository automation. + +The \`github-token\` mode is intentionally explicit. GitHub suppresses workflow +runs caused by most events created with the repository \`GITHUB_TOKEN\`, which +means a pull request created through that mode may not trigger the consumer's +normal pull-request CI. Use it only when that limitation is acceptable or when +another mechanism explicitly triggers validation. + +For GitHub App credentials, request only the repository permissions needed by +the updater: Contents write, Pull requests write and Workflows write. Do not +install or share the LibreCode GitHub App/private key with external consumers. From 24da76b7d6ea394afadb1dd0409132a33433165f Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 18:47:05 -0300 Subject: [PATCH 4/4] fix: keep portable sync auth in upstream patch --- .../sync-workflow-templates.yml.patch | 114 ++++++++++++++++-- 1 file changed, 105 insertions(+), 9 deletions(-) diff --git a/patches/nextcloud/sync-workflow-templates.yml.patch b/patches/nextcloud/sync-workflow-templates.yml.patch index a76932a..59a4472 100644 --- a/patches/nextcloud/sync-workflow-templates.yml.patch +++ b/patches/nextcloud/sync-workflow-templates.yml.patch @@ -1,6 +1,6 @@ --- upstream/vendor/nextcloud/sync-workflow-templates.yml +++ workflow-templates/sync-workflow-templates.yml -@@ -1,6 +1,6 @@ +@@ -1,13 +1,19 @@ # This workflow is provided via the organization template repository # -# https://github.com/nextcloud/.github @@ -8,7 +8,22 @@ # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization # # SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -@@ -26,9 +26,6 @@ + # SPDX-License-Identifier: MIT + +-# This workflow will update all workflow templates +-# Additionally it will reapply `workflow.yml.patch` files after syncing and only then commit the result ++# This workflow will update all workflow templates. ++# Additionally it will reapply workflow.yml.patch files after syncing and only then commit the result. ++# ++# Authentication is explicit through vars.WORKFLOW_SYNC_AUTH_MODE: ++# - librecode-app (default): LibreCode-managed GitHub App credentials ++# - github-app: consumer-owned GitHub App credentials ++# - token: consumer-owned repository-scoped token ++# - github-token: built-in GITHUB_TOKEN (generated PRs do not trigger normal PR workflows) + name: Update workflows + on: + workflow_dispatch: +@@ -26,9 +32,6 @@ matrix: branches: - ${{ github.event.repository.default_branch }} @@ -18,12 +33,52 @@ name: Update workflows in ${{ matrix.branches }} -@@ -42,12 +39,24 @@ +@@ -42,12 +45,105 @@ with: require: admin -+ - name: Create GitHub App token -+ id: app-token ++ - name: Validate workflow sync authentication ++ shell: bash ++ env: ++ AUTH_MODE: ${{ vars.WORKFLOW_SYNC_AUTH_MODE || 'librecode-app' }} ++ LIBRECODE_APP_ID: ${{ vars.LIBRECODE_WORKFLOW_APP_ID }} ++ LIBRECODE_APP_PRIVATE_KEY: ${{ secrets.LIBRECODE_WORKFLOW_APP_PRIVATE_KEY }} ++ CONSUMER_APP_ID: ${{ vars.WORKFLOW_SYNC_APP_ID }} ++ CONSUMER_APP_PRIVATE_KEY: ${{ secrets.WORKFLOW_SYNC_APP_PRIVATE_KEY }} ++ CONSUMER_TOKEN: ${{ secrets.WORKFLOW_SYNC_TOKEN }} ++ run: | ++ set -euo pipefail ++ ++ case "${AUTH_MODE}" in ++ librecode-app) ++ [[ -n "${LIBRECODE_APP_ID}" && -n "${LIBRECODE_APP_PRIVATE_KEY}" ]] || { ++ echo "::error::librecode-app requires LIBRECODE_WORKFLOW_APP_ID and LIBRECODE_WORKFLOW_APP_PRIVATE_KEY" ++ exit 1 ++ } ++ ;; ++ github-app) ++ [[ -n "${CONSUMER_APP_ID}" && -n "${CONSUMER_APP_PRIVATE_KEY}" ]] || { ++ echo "::error::github-app requires WORKFLOW_SYNC_APP_ID and WORKFLOW_SYNC_APP_PRIVATE_KEY" ++ exit 1 ++ } ++ ;; ++ token) ++ [[ -n "${CONSUMER_TOKEN}" ]] || { ++ echo "::error::token mode requires WORKFLOW_SYNC_TOKEN" ++ exit 1 ++ } ++ ;; ++ github-token) ++ ;; ++ *) ++ echo "::error::Unsupported WORKFLOW_SYNC_AUTH_MODE: ${AUTH_MODE}" ++ exit 1 ++ ;; ++ esac ++ ++ - name: Create LibreCode GitHub App token ++ if: ${{ vars.WORKFLOW_SYNC_AUTH_MODE == '' || vars.WORKFLOW_SYNC_AUTH_MODE == 'librecode-app' }} ++ id: librecode-app-token + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + with: + app-id: ${{ vars.LIBRECODE_WORKFLOW_APP_ID }} @@ -33,6 +88,47 @@ + permission-contents: write + permission-pull-requests: write + permission-workflows: write ++ ++ - name: Create consumer GitHub App token ++ if: ${{ vars.WORKFLOW_SYNC_AUTH_MODE == 'github-app' }} ++ id: consumer-app-token ++ uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 ++ with: ++ app-id: ${{ vars.WORKFLOW_SYNC_APP_ID }} ++ private-key: ${{ secrets.WORKFLOW_SYNC_APP_PRIVATE_KEY }} ++ owner: ${{ github.repository_owner }} ++ repositories: ${{ github.event.repository.name }} ++ permission-contents: write ++ permission-pull-requests: write ++ permission-workflows: write ++ ++ - name: Resolve workflow sync token ++ id: auth-token ++ shell: bash ++ env: ++ AUTH_MODE: ${{ vars.WORKFLOW_SYNC_AUTH_MODE || 'librecode-app' }} ++ LIBRECODE_APP_TOKEN: ${{ steps.librecode-app-token.outputs.token }} ++ CONSUMER_APP_TOKEN: ${{ steps.consumer-app-token.outputs.token }} ++ CONSUMER_TOKEN: ${{ secrets.WORKFLOW_SYNC_TOKEN }} ++ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ++ run: | ++ set -euo pipefail ++ ++ case "${AUTH_MODE}" in ++ librecode-app) token="${LIBRECODE_APP_TOKEN}" ;; ++ github-app) token="${CONSUMER_APP_TOKEN}" ;; ++ token) token="${CONSUMER_TOKEN}" ;; ++ github-token) token="${GITHUB_TOKEN}" ;; ++ *) exit 1 ;; ++ esac ++ ++ [[ -n "${token}" ]] || { ++ echo "::error::Selected workflow sync authentication produced an empty token" ++ exit 1 ++ } ++ ++ echo "::add-mask::${token}" ++ echo "token=${token}" >> "${GITHUB_OUTPUT}" + - name: Checkout workflow repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -44,7 +140,7 @@ - name: Checkout app uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 -@@ -56,86 +65,30 @@ +@@ -56,86 +152,30 @@ path: target ref: ${{ matrix.branches }} @@ -123,11 +219,11 @@ uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: - token: ${{ secrets.COMMAND_BOT_WORKFLOWS }} -+ token: ${{ steps.app-token.outputs.token }} ++ token: ${{ steps.auth-token.outputs.token }} commit-message: 'ci(actions): Update workflow templates from organization template repository' committer: GitHub - author: nextcloud-command -+ author: librecode-workflow-automation[bot] <331658022+librecode-workflow-automation[bot]@users.noreply.github.com> ++ author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> path: target signoff: true branch: 'automated/noid/${{ matrix.branches }}-update-workflows' @@ -138,7 +234,7 @@ body: | - Automated update of all workflow templates from [nextcloud/.github](https://github.com/nextcloud/.github) - ${{ env.SUMMARY }} -+ Automated update of all workflow templates from [LibreCodeCoop/.github](https://github.com/LibreCodeCoop/.github) ++ Automated update of workflow templates from [LibreCodeCoop/.github](https://github.com/LibreCodeCoop/.github) + ${{ steps.sync.outputs.summary }} labels: | dependencies