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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/cross-repository-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
114 changes: 105 additions & 9 deletions patches/nextcloud/sync-workflow-templates.yml.patch
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
--- 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
+# https://github.com/LibreCodeCoop/.github
# 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 }}
Expand All @@ -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 }}
Expand All @@ -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
Expand All @@ -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 }}

Expand Down Expand Up @@ -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 <noreply@github.com>
- author: nextcloud-command <nextcloud-command@users.noreply.github.com>
+ 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'
Expand All @@ -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
Expand Down
47 changes: 47 additions & 0 deletions tests/test_portable_workflow_sync_auth.py
Original file line number Diff line number Diff line change
@@ -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()
101 changes: 94 additions & 7 deletions workflow-templates/sync-workflow-templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Expand All @@ -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:
Expand All @@ -76,18 +163,18 @@ 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 <noreply@github.com>
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'
title: '[${{ matrix.branches }}] ci(actions): Update workflow templates from organization template repository'
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
Expand Down
Loading