From f9313e89e0852e751cb8d9b6ebd1da3822e69b89 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:28 -0300 Subject: [PATCH 01/49] feat: move workflow catalog source machinery --- upstream/sources.json | 130 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 upstream/sources.json diff --git a/upstream/sources.json b/upstream/sources.json new file mode 100644 index 0000000..cd37015 --- /dev/null +++ b/upstream/sources.json @@ -0,0 +1,130 @@ +{ + "sources": [ + { + "name": "nextcloud-appstore-build-publish", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/appstore-build-publish.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/fb5f5adee71c8c62b76ec2fb9499261233a5ff6a/workflow-templates/appstore-build-publish.yml", + "sha256": "4710d78c576abd1c875d799770f67262988119404d58d379656f5932872fcba8", + "destination": "upstream/vendor/nextcloud/appstore-build-publish.yml" + }, + { + "name": "nextcloud-block-unconventional-commits", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/block-unconventional-commits.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/98dc00f32e5c4b85361a06a40c7f8d7d1c0a38f3/workflow-templates/block-unconventional-commits.yml", + "sha256": "a123a3045de59f81b67e4554b9e463c1dbcd0ec7db745e53ea3cd7790f5b2e39", + "destination": "upstream/vendor/nextcloud/block-unconventional-commits.yml" + }, + { + "name": "nextcloud-lint-eslint", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/lint-eslint.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/74cadeba1fd707e6a7ae193a8659e6834d6dc2c7/workflow-templates/lint-eslint.yml", + "sha256": "f26cdca597f39732ebb42adba611633ba2c1249d285366bb0d3281f85df5f8dd", + "destination": "upstream/vendor/nextcloud/lint-eslint.yml" + }, + { + "name": "nextcloud-lint-info-xml", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/lint-info-xml.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/98dc00f32e5c4b85361a06a40c7f8d7d1c0a38f3/workflow-templates/lint-info-xml.yml", + "sha256": "cdfd953e3b0b04dfe0ac4c35169a4f6c58a2b62028c918d3d0d1c19e0a3237aa", + "destination": "upstream/vendor/nextcloud/lint-info-xml.yml" + }, + { + "name": "nextcloud-lint-php", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/lint-php.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/fb5f5adee71c8c62b76ec2fb9499261233a5ff6a/workflow-templates/lint-php.yml", + "sha256": "cdf80971a01d51a42cbcf4e0326e3830fa3613b0072b322d5277ba310873dd4b", + "destination": "upstream/vendor/nextcloud/lint-php.yml" + }, + { + "name": "nextcloud-lint-php-cs", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/lint-php-cs.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/fb5f5adee71c8c62b76ec2fb9499261233a5ff6a/workflow-templates/lint-php-cs.yml", + "sha256": "e6047b215571aa2746bd3d2786a14b3e0122fc34e70755a29ff573bb21e1672c", + "destination": "upstream/vendor/nextcloud/lint-php-cs.yml" + }, + { + "name": "nextcloud-lint-stylelint", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/lint-stylelint.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/98dc00f32e5c4b85361a06a40c7f8d7d1c0a38f3/workflow-templates/lint-stylelint.yml", + "sha256": "e66e5c0b4ff2cf6741a1891849ea3de8662c77c6d198131e0cf54ab2a82cccf7", + "destination": "upstream/vendor/nextcloud/lint-stylelint.yml" + }, + { + "name": "nextcloud-lint-typescript", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/lint-typescript.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/cf6248d5ef28a328cde764daf80bc5fae4705ade/workflow-templates/lint-typescript.yml", + "sha256": "2b243e05ff8ac82561a46aab1d485e4a83cf13c939f74b6f978d43ab197ab951", + "destination": "upstream/vendor/nextcloud/lint-typescript.yml" + }, + { + "name": "nextcloud-node-test", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/node-test.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/cf6248d5ef28a328cde764daf80bc5fae4705ade/workflow-templates/node-test.yml", + "sha256": "e6245c256fb6e124678b6d802d2f5133f47ab6d0280b71db1d613d1921343605", + "destination": "upstream/vendor/nextcloud/node-test.yml" + }, + { + "name": "nextcloud-npm-build", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/npm-build.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/74cadeba1fd707e6a7ae193a8659e6834d6dc2c7/workflow-templates/npm-build.yml", + "sha256": "0004666c0ac21d5c10aee4b5912cac74de16f7d2e4e8d0307429aba4e4d1b838", + "destination": "upstream/vendor/nextcloud/npm-build.yml" + }, + { + "name": "nextcloud-openapi", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/openapi.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/fb5f5adee71c8c62b76ec2fb9499261233a5ff6a/workflow-templates/openapi.yml", + "sha256": "9e83f1409b455532b434f7a753b04e1e81a5e6e2a7e1291620ef443dee21bf5c", + "destination": "upstream/vendor/nextcloud/openapi.yml" + }, + { + "name": "nextcloud-psalm", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/psalm.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/fb5f5adee71c8c62b76ec2fb9499261233a5ff6a/workflow-templates/psalm.yml", + "sha256": "02d1db3a1549f5fa933b7157fc1a18d4b4593b35745a1f18be3f49e6e0499bd8", + "destination": "upstream/vendor/nextcloud/psalm.yml" + }, + { + "name": "nextcloud-reuse", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/reuse.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/98dc00f32e5c4b85361a06a40c7f8d7d1c0a38f3/workflow-templates/reuse.yml", + "sha256": "13133274d505be2d100b27637ccd893a24f9dea82cbf96483a1024138aacf5b3", + "destination": "upstream/vendor/nextcloud/reuse.yml" + }, + { + "name": "nextcloud-sync-workflow-templates", + "repository": "nextcloud/.github", + "ref": "master", + "path": "workflow-templates/sync-workflow-templates.yml", + "url": "https://raw.githubusercontent.com/nextcloud/.github/e137952600ed786d2dfd1bf928fadf92d295483c/workflow-templates/sync-workflow-templates.yml", + "sha256": "b1a49a5dbc35ca40a1485bed5f84ef042c6c738f5c81cb229035f24fd6dc0cf0", + "destination": "upstream/vendor/nextcloud/sync-workflow-templates.yml" + } + ] +} From 257c3d93a152f7c3ff1a8ac3583df305e4619802 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:30 -0300 Subject: [PATCH 02/49] feat: move workflow catalog source machinery --- upstream/templates.json | 116 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 upstream/templates.json diff --git a/upstream/templates.json b/upstream/templates.json new file mode 100644 index 0000000..db9334b --- /dev/null +++ b/upstream/templates.json @@ -0,0 +1,116 @@ +{ + "templates": [ + { + "name": "nextcloud-appstore-build-publish", + "source": "upstream/vendor/nextcloud/appstore-build-publish.yml", + "patches": [ + "patches/nextcloud/appstore-build-publish.yml.patch" + ], + "destination": "workflow-templates/appstore-build-publish.yml" + }, + { + "name": "nextcloud-block-unconventional-commits", + "source": "upstream/vendor/nextcloud/block-unconventional-commits.yml", + "patches": [ + "patches/nextcloud/block-unconventional-commits.yml.patch" + ], + "destination": "workflow-templates/block-unconventional-commits.yml" + }, + { + "name": "nextcloud-lint-eslint", + "source": "upstream/vendor/nextcloud/lint-eslint.yml", + "patches": [ + "patches/nextcloud/lint-eslint.yml.patch" + ], + "destination": "workflow-templates/lint-eslint.yml" + }, + { + "name": "nextcloud-lint-info-xml", + "source": "upstream/vendor/nextcloud/lint-info-xml.yml", + "patches": [ + "patches/nextcloud/lint-info-xml.yml.patch" + ], + "destination": "workflow-templates/lint-info-xml.yml" + }, + { + "name": "nextcloud-lint-php", + "source": "upstream/vendor/nextcloud/lint-php.yml", + "patches": [ + "patches/nextcloud/lint-php.yml.patch" + ], + "destination": "workflow-templates/lint-php.yml" + }, + { + "name": "nextcloud-lint-php-cs", + "source": "upstream/vendor/nextcloud/lint-php-cs.yml", + "patches": [ + "patches/nextcloud/lint-php-cs.yml.patch" + ], + "destination": "workflow-templates/lint-php-cs.yml" + }, + { + "name": "nextcloud-lint-stylelint", + "source": "upstream/vendor/nextcloud/lint-stylelint.yml", + "patches": [ + "patches/nextcloud/lint-stylelint.yml.patch" + ], + "destination": "workflow-templates/lint-stylelint.yml" + }, + { + "name": "nextcloud-lint-typescript", + "source": "upstream/vendor/nextcloud/lint-typescript.yml", + "patches": [ + "patches/nextcloud/lint-typescript.yml.patch" + ], + "destination": "workflow-templates/lint-typescript.yml" + }, + { + "name": "nextcloud-node-test", + "source": "upstream/vendor/nextcloud/node-test.yml", + "patches": [ + "patches/nextcloud/node-test.yml.patch" + ], + "destination": "workflow-templates/node-test.yml" + }, + { + "name": "nextcloud-npm-build", + "source": "upstream/vendor/nextcloud/npm-build.yml", + "patches": [ + "patches/nextcloud/npm-build.yml.patch" + ], + "destination": "workflow-templates/npm-build.yml" + }, + { + "name": "nextcloud-openapi", + "source": "upstream/vendor/nextcloud/openapi.yml", + "patches": [ + "patches/nextcloud/openapi.yml.patch" + ], + "destination": "workflow-templates/openapi.yml" + }, + { + "name": "nextcloud-psalm", + "source": "upstream/vendor/nextcloud/psalm.yml", + "patches": [ + "patches/nextcloud/psalm.yml.patch" + ], + "destination": "workflow-templates/psalm.yml" + }, + { + "name": "nextcloud-reuse", + "source": "upstream/vendor/nextcloud/reuse.yml", + "patches": [ + "patches/nextcloud/reuse.yml.patch" + ], + "destination": "workflow-templates/reuse.yml" + }, + { + "name": "nextcloud-sync-workflow-templates", + "source": "upstream/vendor/nextcloud/sync-workflow-templates.yml", + "patches": [ + "patches/nextcloud/sync-workflow-templates.yml.patch" + ], + "destination": "workflow-templates/sync-workflow-templates.yml" + } + ] +} From b6604ef93e58f1f5d925dd079eec322e8081639c Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:33 -0300 Subject: [PATCH 03/49] feat: move workflow catalog source machinery --- scripts/sync_upstream.py | 252 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 scripts/sync_upstream.py diff --git a/scripts/sync_upstream.py b/scripts/sync_upstream.py new file mode 100644 index 0000000..18f8c6f --- /dev/null +++ b/scripts/sync_upstream.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from dataclasses import dataclass +from pathlib import Path +from urllib.parse import quote, urlparse +from urllib.request import Request, urlopen + + +@dataclass(frozen=True) +class Source: + name: str + url: str + sha256: str + destination: Path + repository: str | None = None + ref: str | None = None + path: str | None = None + + +def load_sources(manifest_path: Path) -> list[Source]: + payload = json.loads(manifest_path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError("manifest must be a JSON object") + + raw_sources = payload.get("sources") + if not isinstance(raw_sources, list): + raise ValueError("manifest.sources must be an array") + + sources: list[Source] = [] + for index, raw in enumerate(raw_sources): + if not isinstance(raw, dict): + raise ValueError(f"manifest.sources[{index}] must be an object") + + name = _non_empty_string(raw.get("name"), f"sources[{index}].name") + url = _non_empty_string(raw.get("url"), f"sources[{index}].url") + digest = _non_empty_string(raw.get("sha256"), f"sources[{index}].sha256") + destination = _non_empty_string( + raw.get("destination"), f"sources[{index}].destination" + ) + + if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest): + raise ValueError(f"sources[{index}].sha256 must be 64 lowercase hex characters") + _validate_immutable_url(url, f"sources[{index}].url") + + repository = _optional_string(raw.get("repository"), f"sources[{index}].repository") + ref = _optional_string(raw.get("ref"), f"sources[{index}].ref") + path = _optional_string(raw.get("path"), f"sources[{index}].path") + tracking = (repository, ref, path) + if any(value is not None for value in tracking) and not all( + value is not None for value in tracking + ): + raise ValueError( + f"sources[{index}] must define repository, ref and path together" + ) + + sources.append( + Source( + name=name, + url=url, + sha256=digest, + destination=Path(destination), + repository=repository, + ref=ref, + path=path, + ) + ) + + return sources + + +def fetch(source: Source) -> bytes: + content = _download(source.url) + actual = hashlib.sha256(content).hexdigest() + if actual != source.sha256: + raise ValueError( + f"{source.name}: SHA-256 mismatch: expected {source.sha256}, got {actual}" + ) + return content + + +def sync(sources: list[Source], root: Path) -> None: + for source in sources: + content = fetch(source) + destination = _safe_destination(root, source.destination) + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(content) + + +def check(sources: list[Source], root: Path) -> None: + drift: list[str] = [] + for source in sources: + expected = fetch(source) + destination = _safe_destination(root, source.destination) + if not destination.exists() or destination.read_bytes() != expected: + drift.append(source.name) + + if drift: + raise ValueError("generated templates are out of date: " + ", ".join(drift)) + + +def refresh(manifest_path: Path, root: Path, token: str | None = None) -> None: + payload = json.loads(manifest_path.read_text(encoding="utf-8")) + raw_sources = payload.get("sources") + if not isinstance(raw_sources, list): + raise ValueError("manifest.sources must be an array") + + # Validate the current manifest before mutating it. + load_sources(manifest_path) + + for index, raw in enumerate(raw_sources): + if not isinstance(raw, dict): + raise ValueError(f"manifest.sources[{index}] must be an object") + + repository = raw.get("repository") + ref = raw.get("ref") + path = raw.get("path") + if not all(isinstance(value, str) and value for value in (repository, ref, path)): + continue + + commit = _latest_commit(repository, ref, path, token) + url = f"https://raw.githubusercontent.com/{repository}/{commit}/{path}" + content = _download(url) + digest = hashlib.sha256(content).hexdigest() + + if digest != raw["sha256"]: + raw["url"] = url + raw["sha256"] = digest + + destination = _safe_destination(root, Path(str(raw["destination"]))) + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(content) + + manifest_path.write_text( + json.dumps(payload, indent=2, sort_keys=False) + "\n", + encoding="utf-8", + ) + + +def _latest_commit(repository: str, ref: str, path: str, token: str | None) -> str: + url = ( + f"https://api.github.com/repos/{repository}/commits" + f"?sha={quote(ref, safe='')}&path={quote(path, safe='')}&per_page=1" + ) + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "github-workflows-sync", + "X-GitHub-Api-Version": "2022-11-28", + } + if token: + headers["Authorization"] = f"Bearer {token}" + + request = Request(url, headers=headers) + with urlopen(request, timeout=30) as response: + payload = json.load(response) + + if not isinstance(payload, list) or not payload: + raise ValueError( + f"cannot resolve latest commit for {repository}:{ref}:{path}" + ) + commit = payload[0].get("sha") + if not isinstance(commit, str) or len(commit) != 40: + raise ValueError( + f"invalid commit returned for {repository}:{ref}:{path}" + ) + return commit + + +def _download(url: str) -> bytes: + request = Request(url, headers={"User-Agent": "github-workflows-sync"}) + with urlopen(request, timeout=30) as response: + return response.read() + + +def _validate_immutable_url(url: str, path: str) -> None: + parsed = urlparse(url) + if parsed.scheme != "https": + raise ValueError(f"{path} must use https") + + if parsed.hostname == "raw.githubusercontent.com": + parts = [part for part in parsed.path.split("/") if part] + if len(parts) < 4: + raise ValueError(f"{path} is not a valid raw GitHub file URL") + revision = parts[2] + if len(revision) != 40 or any( + char not in "0123456789abcdefABCDEF" for char in revision + ): + raise ValueError(f"{path} must pin a 40-character Git commit SHA") + elif "/refs/heads/" in parsed.path: + raise ValueError(f"{path} must not reference a mutable branch") + + +def _safe_destination(root: Path, destination: Path) -> Path: + if destination.is_absolute() or ".." in destination.parts: + raise ValueError(f"unsafe destination: {destination}") + resolved = (root / destination).resolve() + root_resolved = root.resolve() + if resolved != root_resolved and root_resolved not in resolved.parents: + raise ValueError(f"destination escapes repository root: {destination}") + return resolved + + +def _non_empty_string(value: object, path: str) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"{path} must be a non-empty string") + return value + + +def _optional_string(value: object, path: str) -> str | None: + if value is None: + return None + if not isinstance(value, str) or not value: + raise ValueError(f"{path} must be a non-empty string when defined") + return value + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("command", choices=("sync", "check", "refresh")) + parser.add_argument("manifest", type=Path) + args = parser.parse_args() + + root = Path.cwd() + + try: + if args.command == "refresh": + refresh( + args.manifest, + root, + token=os.environ.get("GITHUB_TOKEN"), + ) + else: + sources = load_sources(args.manifest) + if args.command == "sync": + sync(sources, root) + else: + check(sources, root) + except ValueError as error: + parser.error(str(error)) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 4ae94b34a7f3bd7229fa6fc74cdab132a090abab Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:36 -0300 Subject: [PATCH 04/49] feat: move workflow catalog source machinery --- scripts/render_upstream.py | 281 +++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 scripts/render_upstream.py diff --git a/scripts/render_upstream.py b/scripts/render_upstream.py new file mode 100644 index 0000000..60e1bbc --- /dev/null +++ b/scripts/render_upstream.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +from __future__ import annotations + +import argparse +import json +import shutil +import subprocess +import tempfile +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class Template: + name: str + source: Path + patches: tuple[Path, ...] + destination: Path + + +def load_templates(manifest_path: Path) -> list[Template]: + payload = json.loads(manifest_path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError("manifest must be a JSON object") + + raw_templates = payload.get("templates") + if not isinstance(raw_templates, list): + raise ValueError("manifest.templates must be an array") + + templates: list[Template] = [] + for index, raw in enumerate(raw_templates): + if not isinstance(raw, dict): + raise ValueError(f"manifest.templates[{index}] must be an object") + + name = _non_empty_string(raw.get("name"), f"templates[{index}].name") + source = Path(_non_empty_string(raw.get("source"), f"templates[{index}].source")) + destination = Path( + _non_empty_string(raw.get("destination"), f"templates[{index}].destination") + ) + + raw_patches = raw.get("patches", []) + if not isinstance(raw_patches, list) or not all( + isinstance(item, str) and item for item in raw_patches + ): + raise ValueError(f"templates[{index}].patches must be an array of paths") + + for path in (source, destination, *(Path(item) for item in raw_patches)): + _validate_relative_path(path) + + templates.append( + Template( + name=name, + source=source, + patches=tuple(Path(item) for item in raw_patches), + destination=destination, + ) + ) + + return templates + + +def render(template: Template, root: Path) -> bytes: + source = _safe_path(root, template.source) + if not source.is_file(): + raise ValueError(f"source does not exist: {template.source}") + + with tempfile.TemporaryDirectory() as directory: + working = Path(directory) / source.name + shutil.copyfile(source, working) + + for patch_path in template.patches: + patch = _safe_path(root, patch_path) + if not patch.is_file(): + raise ValueError(f"patch does not exist: {patch_path}") + + result = subprocess.run( + ["patch", "--batch", "--forward", str(working), str(patch)], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + details = (result.stderr or result.stdout).strip() + raise ValueError(f"failed to apply {patch_path}: {details}") + + return working.read_bytes() + + +def sync(templates: list[Template], root: Path) -> dict[str, object]: + results: list[dict[str, object]] = [] + + for template in templates: + destination = _safe_path(root, template.destination) + try: + content = render(template, root) + previous = destination.read_bytes() if destination.is_file() else None + changed = previous != content + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(content) + results.append( + { + "name": template.name, + "status": "updated" if changed else "unchanged", + "destination": str(template.destination), + "patches": [str(path) for path in template.patches], + } + ) + except ValueError as error: + results.append( + { + "name": template.name, + "status": "failed", + "destination": str(template.destination), + "patches": [str(path) for path in template.patches], + "error": str(error), + } + ) + + counts = { + status: sum(1 for item in results if item["status"] == status) + for status in ("updated", "unchanged", "failed") + } + return { + "ok": counts["failed"] == 0, + **counts, + "templates": results, + } + + +def check(templates: list[Template], root: Path) -> None: + problems: list[str] = [] + for template in templates: + try: + expected = render(template, root) + except ValueError as error: + problems.append(f"{template.name}: {error}") + continue + + destination = _safe_path(root, template.destination) + if not destination.is_file() or destination.read_bytes() != expected: + problems.append(f"{template.name}: rendered template is out of date") + + if problems: + raise ValueError("; ".join(problems)) + + +def write_report(report: dict[str, object], path: Path) -> None: + path.write_text( + json.dumps(report, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def render_pull_request_body(report: dict[str, object] | None) -> str: + lines = [ + "Automated refresh of tracked upstream workflow sources.", + "", + "The source URLs in this change are pinned to immutable commit SHAs " + "and SHA-256 hashes.", + "", + "## Patch status", + "", + ] + + if report is None: + lines.append("No patch report was produced. Review the workflow run before merging.") + else: + lines.extend( + [ + f"- Updated templates: {report['updated']}", + f"- Unchanged templates: {report['unchanged']}", + f"- Failed templates: {report['failed']}", + "", + ] + ) + + for item in report["templates"]: + status = item["status"] + icon = {"updated": "✅", "unchanged": "➖", "failed": "❌"}[status] + lines.append(f"### {icon} {item['name']} — {status}") + lines.append("") + lines.append(f"Destination: {item['destination']}") + + patches = item["patches"] + if patches: + lines.extend(["", "Patches:"]) + lines.extend(f"- {patch}" for patch in patches) + + if status == "failed": + lines.extend( + [ + "", + "Patch application failed:", + "", + "~~~text", + str(item["error"]), + "~~~", + "", + "The vendored upstream source was updated, but the generated " + "template was left unchanged and needs manual patch adjustment.", + ] + ) + + lines.append("") + + lines.extend( + [ + "Review upstream changes and downstream patches before merging.", + "", + ] + ) + return "\n".join(lines) + + +def write_pull_request_body(report_path: Path, output_path: Path) -> None: + report = None + if report_path.is_file(): + report = json.loads(report_path.read_text(encoding="utf-8")) + output_path.write_text(render_pull_request_body(report), encoding="utf-8") + + +def _validate_relative_path(path: Path) -> None: + if path.is_absolute() or ".." in path.parts: + raise ValueError(f"unsafe path: {path}") + + +def _safe_path(root: Path, path: Path) -> Path: + _validate_relative_path(path) + resolved = (root / path).resolve() + root_resolved = root.resolve() + if resolved != root_resolved and root_resolved not in resolved.parents: + raise ValueError(f"path escapes repository root: {path}") + return resolved + + +def _non_empty_string(value: object, path: str) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"{path} must be a non-empty string") + return value + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("command", choices=("sync", "check", "pr-body")) + parser.add_argument("manifest", type=Path, nargs="?") + parser.add_argument("--report", type=Path) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + + root = Path.cwd() + + try: + if args.command == "pr-body": + if args.report is None or args.output is None: + parser.error("pr-body requires --report and --output") + write_pull_request_body(args.report, args.output) + return 0 + + if args.manifest is None: + parser.error(f"{args.command} requires a manifest") + + templates = load_templates(args.manifest) + if args.command == "sync": + report = sync(templates, root) + if args.report: + write_report(report, args.report) + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 if report["ok"] else 1 + + check(templates, root) + except (OSError, ValueError) as error: + parser.error(str(error)) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 284ff8ab615edfefcbcfb8ff92f9ad02701cbdb9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:38 -0300 Subject: [PATCH 05/49] feat: move workflow catalog source machinery --- scripts/check_workflow_policy.py | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 scripts/check_workflow_policy.py diff --git a/scripts/check_workflow_policy.py b/scripts/check_workflow_policy.py new file mode 100644 index 0000000..6f54727 --- /dev/null +++ b/scripts/check_workflow_policy.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + +USES_RE = re.compile(r"^\s*(?:-\s*)?uses:\s*([^@\s]+)@([^\s#]+)") +FULL_SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") + + +def workflow_files(root: Path) -> list[Path]: + return sorted( + path + for path in root.rglob("*") + if path.is_file() and path.suffix in {".yml", ".yaml"} + ) + + +def check_file(path: Path) -> list[str]: + lines = path.read_text(encoding="utf-8").splitlines() + findings: list[str] = [] + + for index, line in enumerate(lines): + line_number = index + 1 + + if re.search(r"permissions:\s*write-all\b", line): + findings.append(f"{path}:{line_number}: permissions: write-all is forbidden") + + match = USES_RE.match(line) + if not match: + continue + + action, revision = match.groups() + if action.startswith("./"): + continue + + if not FULL_SHA_RE.fullmatch(revision): + findings.append( + f"{path}:{line_number}: {action} must be pinned to a full 40-character commit SHA" + ) + + if action == "actions/checkout": + block = "\n".join(lines[index + 1 : index + 8]) + if not re.search(r"persist-credentials:\s*false\b", block): + findings.append( + f"{path}:{line_number}: actions/checkout must set persist-credentials: false" + ) + + return findings + + +def check_roots(roots: list[Path]) -> list[str]: + findings: list[str] = [] + for root in roots: + if not root.exists(): + continue + for path in workflow_files(root): + findings.extend(check_file(path)) + return findings + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "roots", + nargs="*", + type=Path, + default=[Path("workflow-templates"), Path(".github/workflows")], + ) + args = parser.parse_args() + + findings = check_roots(args.roots) + if findings: + print("\n".join(findings)) + return 1 + + print("Workflow policy checks passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 3af712cd1d474f0366051bff9325f061d6312c4d Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:41 -0300 Subject: [PATCH 06/49] feat: move workflow catalog source machinery --- tests/test_sync_upstream.py | 286 ++++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 tests/test_sync_upstream.py diff --git a/tests/test_sync_upstream.py b/tests/test_sync_upstream.py new file mode 100644 index 0000000..9769717 --- /dev/null +++ b/tests/test_sync_upstream.py @@ -0,0 +1,286 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +import hashlib +import json +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from scripts.sync_upstream import Source, check, load_sources, refresh, sync + + +class SyncUpstreamTest(unittest.TestCase): + def test_loads_valid_manifest(self) -> None: + with tempfile.TemporaryDirectory() as directory: + manifest = Path(directory) / "sources.json" + manifest.write_text( + json.dumps( + { + "sources": [ + { + "name": "workflow", + "repository": "example/project", + "ref": "master", + "path": "workflow.yml", + "url": "https://raw.githubusercontent.com/example/project/0123456789012345678901234567890123456789/workflow.yml", + "sha256": "a" * 64, + "destination": "templates/workflow.yml", + } + ] + } + ), + encoding="utf-8", + ) + + [source] = load_sources(manifest) + self.assertEqual(source.name, "workflow") + self.assertEqual(source.repository, "example/project") + self.assertEqual(source.ref, "master") + self.assertEqual(source.path, "workflow.yml") + self.assertEqual(source.destination, Path("templates/workflow.yml")) + + def test_rejects_partial_tracking_metadata(self) -> None: + with tempfile.TemporaryDirectory() as directory: + manifest = Path(directory) / "sources.json" + manifest.write_text( + json.dumps( + { + "sources": [ + { + "name": "workflow", + "repository": "example/project", + "url": "https://raw.githubusercontent.com/example/project/0123456789012345678901234567890123456789/workflow.yml", + "sha256": "a" * 64, + "destination": "templates/workflow.yml", + } + ] + } + ), + encoding="utf-8", + ) + + with self.assertRaisesRegex( + ValueError, "must define repository, ref and path together" + ): + load_sources(manifest) + + def test_rejects_invalid_hash(self) -> None: + with tempfile.TemporaryDirectory() as directory: + manifest = Path(directory) / "sources.json" + manifest.write_text( + json.dumps( + { + "sources": [ + { + "name": "workflow", + "url": "https://example.invalid/commit/workflow.yml", + "sha256": "not-a-hash", + "destination": "templates/workflow.yml", + } + ] + } + ), + encoding="utf-8", + ) + + with self.assertRaisesRegex(ValueError, "sha256"): + load_sources(manifest) + + def test_rejects_mutable_raw_github_revision(self) -> None: + with tempfile.TemporaryDirectory() as directory: + manifest = Path(directory) / "sources.json" + manifest.write_text( + json.dumps( + { + "sources": [ + { + "name": "workflow", + "url": "https://raw.githubusercontent.com/example/project/master/workflow.yml", + "sha256": "a" * 64, + "destination": "templates/workflow.yml", + } + ] + } + ), + encoding="utf-8", + ) + + with self.assertRaisesRegex(ValueError, "40-character Git commit SHA"): + load_sources(manifest) + + def test_sync_writes_verified_content(self) -> None: + content = b"name: Example\n" + source = Source( + name="workflow", + url="https://example.invalid/workflow.yml", + sha256=hashlib.sha256(content).hexdigest(), + destination=Path("templates/workflow.yml"), + ) + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + with patch("scripts.sync_upstream.fetch", return_value=content): + sync([source], root) + + self.assertEqual( + (root / "templates/workflow.yml").read_bytes(), + content, + ) + + def test_sync_rejects_sha_mismatch_without_writing_destination(self) -> None: + expected = b"name: Expected\n" + downloaded = b"name: Unexpected\n" + source = Source( + name="workflow", + url="https://example.invalid/workflow.yml", + sha256=hashlib.sha256(expected).hexdigest(), + destination=Path("templates/workflow.yml"), + ) + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + destination = root / "templates/workflow.yml" + + with patch("scripts.sync_upstream._download", return_value=downloaded): + with self.assertRaisesRegex(ValueError, "SHA-256 mismatch"): + sync([source], root) + + self.assertFalse(destination.exists()) + + def test_check_detects_drift(self) -> None: + expected = b"name: Expected\n" + source = Source( + name="workflow", + url="https://example.invalid/workflow.yml", + sha256=hashlib.sha256(expected).hexdigest(), + destination=Path("templates/workflow.yml"), + ) + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "templates").mkdir() + (root / "templates/workflow.yml").write_bytes(b"name: Old\n") + + with patch("scripts.sync_upstream.fetch", return_value=expected): + with self.assertRaisesRegex(ValueError, "out of date"): + check([source], root) + + @patch("scripts.sync_upstream._download") + @patch("scripts.sync_upstream._latest_commit") + def test_refresh_updates_pin_hash_and_vendor_copy( + self, latest_commit, download + ) -> None: + latest_commit.return_value = "1" * 40 + content = b"name: Updated\n" + download.return_value = content + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + manifest = root / "sources.json" + manifest.write_text( + json.dumps( + { + "sources": [ + { + "name": "workflow", + "repository": "example/project", + "ref": "master", + "path": "workflow.yml", + "url": "https://raw.githubusercontent.com/example/project/0123456789012345678901234567890123456789/workflow.yml", + "sha256": "a" * 64, + "destination": "templates/workflow.yml", + } + ] + } + ), + encoding="utf-8", + ) + + refresh(manifest, root, token="token") + + payload = json.loads(manifest.read_text(encoding="utf-8")) + [source] = payload["sources"] + self.assertEqual( + source["url"], + "https://raw.githubusercontent.com/example/project/" + + "1" * 40 + + "/workflow.yml", + ) + self.assertEqual( + source["sha256"], + hashlib.sha256(content).hexdigest(), + ) + self.assertEqual( + (root / "templates/workflow.yml").read_bytes(), + content, + ) + latest_commit.assert_called_once_with( + "example/project", "master", "workflow.yml", "token" + ) + + @patch("scripts.sync_upstream._download") + @patch("scripts.sync_upstream._latest_commit") + def test_refresh_keeps_existing_pin_when_content_is_unchanged( + self, latest_commit, download + ) -> None: + old_commit = "0" * 40 + new_commit = "1" * 40 + content = b"name: Same\n" + digest = hashlib.sha256(content).hexdigest() + latest_commit.return_value = new_commit + download.return_value = content + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + manifest = root / "sources.json" + original_url = ( + "https://raw.githubusercontent.com/example/project/" + + old_commit + + "/workflow.yml" + ) + manifest.write_text( + json.dumps( + { + "sources": [ + { + "name": "workflow", + "repository": "example/project", + "ref": "master", + "path": "workflow.yml", + "url": original_url, + "sha256": digest, + "destination": "templates/workflow.yml", + } + ] + } + ), + encoding="utf-8", + ) + + refresh(manifest, root, token="token") + + payload = json.loads(manifest.read_text(encoding="utf-8")) + [source] = payload["sources"] + self.assertEqual(source["url"], original_url) + self.assertEqual(source["sha256"], digest) + self.assertEqual((root / "templates/workflow.yml").read_bytes(), content) + + def test_rejects_destination_escape(self) -> None: + content = b"name: Example\n" + source = Source( + name="workflow", + url="https://example.invalid/workflow.yml", + sha256=hashlib.sha256(content).hexdigest(), + destination=Path("../outside.yml"), + ) + + with tempfile.TemporaryDirectory() as directory: + with patch("scripts.sync_upstream.fetch", return_value=content): + with self.assertRaisesRegex(ValueError, "unsafe destination"): + sync([source], Path(directory)) + + +if __name__ == "__main__": + unittest.main() From 89d4bd221ea40fbd577f2fe13de1ce61e0d11c24 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:44 -0300 Subject: [PATCH 07/49] feat: move workflow catalog source machinery --- tests/test_render_upstream.py | 263 ++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 tests/test_render_upstream.py diff --git a/tests/test_render_upstream.py b/tests/test_render_upstream.py new file mode 100644 index 0000000..5234f05 --- /dev/null +++ b/tests/test_render_upstream.py @@ -0,0 +1,263 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +import json +import tempfile +import unittest +from pathlib import Path + +from scripts.render_upstream import ( + check, + load_templates, + render_pull_request_body, + sync, + write_pull_request_body, + write_report, +) + + +class RenderUpstreamTest(unittest.TestCase): + def fixture(self, directory: str) -> tuple[Path, Path]: + root = Path(directory) + source = root / "upstream/vendor/example.yml" + source.parent.mkdir(parents=True) + source.write_text( + "name: Example\n\njobs:\n test:\n runs-on: ubuntu-latest\n", + encoding="utf-8", + ) + + patch = root / "patches/example.yml.patch" + patch.parent.mkdir(parents=True) + patch.write_text( + "--- example.yml\n" + "+++ example.yml\n" + "@@ -1,5 +1,5 @@\n" + "-name: Example\n" + "+name: Patched example\n" + " \n" + " jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n", + encoding="utf-8", + ) + + manifest = root / "upstream/templates.json" + manifest.parent.mkdir(parents=True, exist_ok=True) + manifest.write_text( + json.dumps( + { + "templates": [ + { + "name": "example", + "source": "upstream/vendor/example.yml", + "patches": ["patches/example.yml.patch"], + "destination": "workflow-templates/example.yml", + } + ] + } + ), + encoding="utf-8", + ) + return root, manifest + + def test_sync_applies_patch(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root, manifest = self.fixture(directory) + report = sync(load_templates(manifest), root) + rendered = (root / "workflow-templates/example.yml").read_text(encoding="utf-8") + self.assertIn("name: Patched example", rendered) + self.assertTrue(report["ok"]) + self.assertEqual(report["updated"], 1) + self.assertEqual(report["failed"], 0) + + def test_sync_reports_failure_and_continues(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root, manifest = self.fixture(directory) + + good_source = root / "upstream/vendor/good.yml" + good_source.write_text("name: Good\n", encoding="utf-8") + + broken_source = root / "upstream/vendor/broken.yml" + broken_source.write_text("name: Changed upstream\n", encoding="utf-8") + broken_patch = root / "patches/broken.yml.patch" + broken_patch.write_text( + "--- broken.yml\n" + "+++ broken.yml\n" + "@@ -1 +1 @@\n" + "-name: Old upstream\n" + "+name: Patched\n", + encoding="utf-8", + ) + + manifest.write_text( + json.dumps( + { + "templates": [ + { + "name": "good", + "source": "upstream/vendor/good.yml", + "patches": [], + "destination": "workflow-templates/good.yml", + }, + { + "name": "broken", + "source": "upstream/vendor/broken.yml", + "patches": ["patches/broken.yml.patch"], + "destination": "workflow-templates/broken.yml", + }, + ] + } + ), + encoding="utf-8", + ) + + report = sync(load_templates(manifest), root) + + self.assertFalse(report["ok"]) + self.assertEqual(report["updated"], 1) + self.assertEqual(report["failed"], 1) + self.assertEqual( + (root / "workflow-templates/good.yml").read_text(encoding="utf-8"), + "name: Good\n", + ) + self.assertFalse((root / "workflow-templates/broken.yml").exists()) + failed = next( + item for item in report["templates"] if item["status"] == "failed" + ) + self.assertEqual(failed["name"], "broken") + self.assertIn("failed to apply", failed["error"]) + + def test_failed_patch_preserves_previous_generated_template(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root, manifest = self.fixture(directory) + source = root / "upstream/vendor/example.yml" + source.write_text("name: Changed upstream\n", encoding="utf-8") + destination = root / "workflow-templates/example.yml" + destination.parent.mkdir(parents=True) + destination.write_text("name: Last known good\n", encoding="utf-8") + + report = sync(load_templates(manifest), root) + + self.assertFalse(report["ok"]) + self.assertEqual(report["failed"], 1) + self.assertEqual( + destination.read_text(encoding="utf-8"), + "name: Last known good\n", + ) + + def test_write_report(self) -> None: + with tempfile.TemporaryDirectory() as directory: + report_path = Path(directory) / "report.json" + report = { + "ok": False, + "updated": 1, + "unchanged": 0, + "failed": 1, + "templates": [], + } + write_report(report, report_path) + self.assertEqual( + json.loads(report_path.read_text(encoding="utf-8")), + report, + ) + + def test_check_detects_rendered_drift(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root, manifest = self.fixture(directory) + destination = root / "workflow-templates/example.yml" + destination.parent.mkdir(parents=True) + destination.write_text("name: stale\n", encoding="utf-8") + + with self.assertRaisesRegex(ValueError, "rendered template is out of date"): + check(load_templates(manifest), root) + + def test_rejects_unsafe_paths(self) -> None: + with tempfile.TemporaryDirectory() as directory: + manifest = Path(directory) / "templates.json" + manifest.write_text( + json.dumps( + { + "templates": [ + { + "name": "example", + "source": "../example.yml", + "patches": [], + "destination": "workflow-templates/example.yml", + } + ] + } + ), + encoding="utf-8", + ) + + with self.assertRaisesRegex(ValueError, "unsafe path"): + load_templates(manifest) + + def test_render_pull_request_body_for_successful_report(self) -> None: + body = render_pull_request_body( + { + "ok": True, + "updated": 1, + "unchanged": 1, + "failed": 0, + "templates": [ + { + "name": "updated", + "status": "updated", + "destination": "workflow-templates/updated.yml", + "patches": ["patches/updated.patch"], + }, + { + "name": "unchanged", + "status": "unchanged", + "destination": "workflow-templates/unchanged.yml", + "patches": [], + }, + ], + } + ) + + self.assertIn("Updated templates: 1", body) + self.assertIn("✅ updated — updated", body) + self.assertIn("➖ unchanged — unchanged", body) + self.assertIn("- patches/updated.patch", body) + + def test_render_pull_request_body_for_failed_patch(self) -> None: + body = render_pull_request_body( + { + "ok": False, + "updated": 0, + "unchanged": 0, + "failed": 1, + "templates": [ + { + "name": "broken", + "status": "failed", + "destination": "workflow-templates/broken.yml", + "patches": ["patches/broken.patch"], + "error": "failed to apply patches/broken.patch", + } + ], + } + ) + + self.assertIn("Failed templates: 1", body) + self.assertIn("❌ broken — failed", body) + self.assertIn("failed to apply patches/broken.patch", body) + self.assertIn("left unchanged", body) + + def test_write_pull_request_body_without_report(self) -> None: + with tempfile.TemporaryDirectory() as directory: + report = Path(directory) / "missing.json" + output = Path(directory) / "body.md" + + write_pull_request_body(report, output) + + self.assertIn( + "No patch report was produced", + output.read_text(encoding="utf-8"), + ) + + +if __name__ == "__main__": + unittest.main() From 5188d778d540bdef0c60862d6db89334cb4c76e4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:46 -0300 Subject: [PATCH 08/49] feat: move workflow catalog source machinery --- tests/test_workflow_policy.py | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_workflow_policy.py diff --git a/tests/test_workflow_policy.py b/tests/test_workflow_policy.py new file mode 100644 index 0000000..90c8372 --- /dev/null +++ b/tests/test_workflow_policy.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +import tempfile +import unittest +from pathlib import Path + +from scripts.check_workflow_policy import check_file + + +class WorkflowPolicyTest(unittest.TestCase): + def write(self, content: str) -> Path: + temporary = tempfile.NamedTemporaryFile(suffix=".yml", delete=False) + path = Path(temporary.name) + temporary.close() + path.write_text(content, encoding="utf-8") + self.addCleanup(path.unlink) + return path + + def test_accepts_pinned_action_and_hardened_checkout(self) -> None: + path = self.write( + """ +permissions: + contents: read +steps: + - uses: actions/checkout@0123456789012345678901234567890123456789 + with: + persist-credentials: false + - uses: example/action@abcdefabcdefabcdefabcdefabcdefabcdefabcd +""" + ) + self.assertEqual(check_file(path), []) + + def test_rejects_mutable_action_revision(self) -> None: + path = self.write("steps:\n - uses: example/action@v2\n") + findings = check_file(path) + self.assertEqual(len(findings), 1) + self.assertIn("full 40-character commit SHA", findings[0]) + + def test_rejects_checkout_with_persisted_credentials(self) -> None: + path = self.write( + """ +steps: + - uses: actions/checkout@0123456789012345678901234567890123456789 +""" + ) + findings = check_file(path) + self.assertEqual(len(findings), 1) + self.assertIn("persist-credentials: false", findings[0]) + + def test_rejects_write_all(self) -> None: + path = self.write("permissions: write-all\n") + findings = check_file(path) + self.assertEqual(len(findings), 1) + self.assertIn("write-all", findings[0]) + + def test_allows_local_action(self) -> None: + path = self.write("steps:\n - uses: ./actions/sync-workflows\n") + self.assertEqual(check_file(path), []) + + +if __name__ == "__main__": + unittest.main() From d41a85fb97e9f8a0985700b1007d35b173b03df0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:53 -0300 Subject: [PATCH 09/49] feat: move workflow catalog source data --- .../nextcloud/appstore-build-publish.yml | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 upstream/vendor/nextcloud/appstore-build-publish.yml diff --git a/upstream/vendor/nextcloud/appstore-build-publish.yml b/upstream/vendor/nextcloud/appstore-build-publish.yml new file mode 100644 index 0000000..699f6d6 --- /dev/null +++ b/upstream/vendor/nextcloud/appstore-build-publish.yml @@ -0,0 +1,202 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Build and publish app release + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + build_and_publish: + runs-on: ubuntu-latest + + # Only allowed to be run on nextcloud-releases repositories + if: ${{ github.repository_owner == 'nextcloud-releases' }} + + steps: + - name: Check actor permission + uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 + with: + require: write + + - name: Set app env + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + path: ${{ env.APP_NAME }} + + - name: Get app version number + id: app-version + uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0 + with: + filename: ${{ env.APP_NAME }}/appinfo/info.xml + expression: "//info//version/text()" + + - name: Validate app version against tag + run: | + [ "${{ env.APP_VERSION }}" = "v${{ fromJSON(steps.app-version.outputs.result).version }}" ] + + - name: Get appinfo data + id: appinfo + uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # v1.0.0 + with: + filename: ${{ env.APP_NAME }}/appinfo/info.xml + expression: "//info//dependencies//nextcloud/@min-version" + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + # Continue if no package.json + continue-on-error: true + with: + path: ${{ env.APP_NAME }} + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + # Skip if no package.json + if: ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + package-manager-cache: false + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + # Skip if no package.json + if: ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Get php version + id: php-versions + uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 + with: + filename: ${{ env.APP_NAME }}/appinfo/info.xml + + - name: Set up php ${{ steps.php-versions.outputs.php-min }} + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ steps.php-versions.outputs.php-min }} + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check composer.json + id: check_composer + uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 + with: + files: "${{ env.APP_NAME }}/composer.json" + + - name: Install composer dependencies + if: steps.check_composer.outputs.files_exists == 'true' + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + composer-options: '--no-dev' + working-directory: ${{ env.APP_NAME }} + ignore-cache: 'yes' + + - name: Build ${{ env.APP_NAME }} + # Skip if no package.json + if: ${{ steps.versions.outputs.nodeVersion }} + env: + CYPRESS_INSTALL_BINARY: 0 + run: | + cd ${{ env.APP_NAME }} + npm ci + npm run build --if-present + + - name: Check Krankerl config + id: krankerl + uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 + with: + files: ${{ env.APP_NAME }}/krankerl.toml + + - name: Install Krankerl + if: steps.krankerl.outputs.files_exists == 'true' + run: | + wget https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb + sudo dpkg -i krankerl_0.14.0_amd64.deb + + - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with krankerl + if: steps.krankerl.outputs.files_exists == 'true' + run: | + cd ${{ env.APP_NAME }} + krankerl package + + - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with makefile + if: steps.krankerl.outputs.files_exists != 'true' + run: | + cd ${{ env.APP_NAME }} + make appstore + + - name: Check server download link for ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + run: | + NCVERSION='${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}' + DOWNLOAD_URL=$(curl -s "https://updates.nextcloud.com/updater_server/latest?channel=beta&version=$NCVERSION" | jq -r '.downloads.zip[0]') + echo "DOWNLOAD_URL=$DOWNLOAD_URL" >> $GITHUB_ENV + + - name: Download server ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + continue-on-error: true + id: server-download + if: ${{ env.DOWNLOAD_URL != 'null' }} + run: | + echo "Downloading release tarball from $DOWNLOAD_URL" + wget $DOWNLOAD_URL -O nextcloud.zip + unzip nextcloud.zip + + - name: Checkout server master fallback + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: ${{ steps.server-download.outcome != 'success' }} + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + path: nextcloud + + + - name: Sign app + run: | + # Extracting release + cd ${{ env.APP_NAME }}/build/artifacts + tar -xvf ${{ env.APP_NAME }}.tar.gz + cd ../../../ + # Setting up keys + echo '${{ secrets.APP_PRIVATE_KEY }}' > ${{ env.APP_NAME }}.key + wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${{ env.APP_NAME }}/${{ env.APP_NAME }}.crt" + # Signing + php nextcloud/occ integrity:sign-app --privateKey=../${{ env.APP_NAME }}.key --certificate=../${{ env.APP_NAME }}.crt --path=../${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }} + # Rebuilding archive + cd ${{ env.APP_NAME }}/build/artifacts + tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }} + + - name: Attach tarball to github release + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5 + id: attach_to_release + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz + asset_name: ${{ env.APP_NAME }}-${{ env.APP_VERSION }}.tar.gz + tag: ${{ github.ref }} + overwrite: true + + - name: Upload app to Nextcloud appstore + uses: nextcloud-libraries/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1.0.3 + with: + app_name: ${{ env.APP_NAME }} + appstore_token: ${{ secrets.APPSTORE_TOKEN }} + download_url: ${{ steps.attach_to_release.outputs.browser_download_url }} + app_private_key: ${{ secrets.APP_PRIVATE_KEY }} From 820a30e12537eae298931dba7157efb9779386b9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:55 -0300 Subject: [PATCH 10/49] feat: move workflow catalog source data --- .../appstore-build-publish.yml.patch | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 patches/nextcloud/appstore-build-publish.yml.patch diff --git a/patches/nextcloud/appstore-build-publish.yml.patch b/patches/nextcloud/appstore-build-publish.yml.patch new file mode 100644 index 0000000..8287eb6 --- /dev/null +++ b/patches/nextcloud/appstore-build-publish.yml.patch @@ -0,0 +1,64 @@ +--- upstream/vendor/nextcloud/appstore-build-publish.yml ++++ workflow-templates/appstore-build-publish.yml +@@ -1,6 +1,6 @@ + # 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: 2021-2024 Nextcloud GmbH and Nextcloud contributors +@@ -19,9 +19,6 @@ + build_and_publish: + runs-on: ubuntu-latest + +- # Only allowed to be run on nextcloud-releases repositories +- if: ${{ github.repository_owner == 'nextcloud-releases' }} +- + steps: + - name: Check actor permission + uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 +@@ -143,6 +140,14 @@ + cd ${{ env.APP_NAME }} + make appstore + ++ - name: Verify app store package ++ if: steps.krankerl.outputs.files_exists != 'true' ++ working-directory: ${{ env.APP_NAME }} ++ run: | ++ if make -qp 2>/dev/null | grep -q '^verify-appstore-package:'; then ++ make verify-appstore-package ++ fi ++ + - name: Check server download link for ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + run: | + NCVERSION='${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}' +@@ -183,6 +188,13 @@ + cd ${{ env.APP_NAME }}/build/artifacts + tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }} + ++ - name: Validate release artifact ++ uses: LibreCodeCoop/github-workflows/actions/release-artifact-validate@622599cc128ec55007b443d2908f78da930b8a21 # v0.5.0 ++ with: ++ artifact: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz ++ app-name: ${{ env.APP_NAME }} ++ version: ${{ env.APP_VERSION }} ++ + - name: Attach tarball to github release + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5 + id: attach_to_release +@@ -196,7 +208,14 @@ + - name: Upload app to Nextcloud appstore + uses: nextcloud-libraries/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1.0.3 + with: + app_name: ${{ env.APP_NAME }} + appstore_token: ${{ secrets.APPSTORE_TOKEN }} + download_url: ${{ steps.attach_to_release.outputs.browser_download_url }} + app_private_key: ${{ secrets.APP_PRIVATE_KEY }} ++ ++ - name: Verify App Store publication ++ uses: LibreCodeCoop/github-workflows/actions/verify-appstore-publication@0bb30136a7fcb7e70b7ae579addeeda882f4c457 # v0.2.0 ++ with: ++ app-name: ${{ env.APP_NAME }} ++ version: ${{ env.APP_VERSION }} ++ nextcloud-platform: ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} From bcb3e8ef514730ca2275f79c1877836f1b9ee3b1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:49:58 -0300 Subject: [PATCH 11/49] feat: move workflow catalog source data --- .../block-unconventional-commits.yml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 upstream/vendor/nextcloud/block-unconventional-commits.yml diff --git a/upstream/vendor/nextcloud/block-unconventional-commits.yml b/upstream/vendor/nextcloud/block-unconventional-commits.yml new file mode 100644 index 0000000..4972f8b --- /dev/null +++ b/upstream/vendor/nextcloud/block-unconventional-commits.yml @@ -0,0 +1,36 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Block unconventional commits + +on: + pull_request: + types: [opened, ready_for_review, reopened, synchronize] + +permissions: + contents: read + +concurrency: + group: block-unconventional-commits-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + block-unconventional-commits: + name: Block unconventional commits + + runs-on: ubuntu-latest-low + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: webiny/action-conventional-commits@7f91b1595ca1951cdb671ddc9f07a49081ec5b69 # v1.4.2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 7c0b112f065561668e6226c8f346ca5160c9a913 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:01 -0300 Subject: [PATCH 12/49] feat: move workflow catalog source data --- .../block-unconventional-commits.yml.patch | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 patches/nextcloud/block-unconventional-commits.yml.patch diff --git a/patches/nextcloud/block-unconventional-commits.yml.patch b/patches/nextcloud/block-unconventional-commits.yml.patch new file mode 100644 index 0000000..579dfe9 --- /dev/null +++ b/patches/nextcloud/block-unconventional-commits.yml.patch @@ -0,0 +1,19 @@ +--- upstream/vendor/nextcloud/block-unconventional-commits.yml ++++ workflow-templates/block-unconventional-commits.yml +@@ -1,6 +1,6 @@ + # 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: 2024 Nextcloud GmbH and Nextcloud contributors +@@ -23,7 +23,7 @@ + block-unconventional-commits: + name: Block unconventional commits + +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + + steps: + - name: Checkout From b9d93a2b556b26a2206ae08d05c9650df8e41123 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:05 -0300 Subject: [PATCH 13/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/lint-eslint.yml | 100 ++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 upstream/vendor/nextcloud/lint-eslint.yml diff --git a/upstream/vendor/nextcloud/lint-eslint.yml b/upstream/vendor/nextcloud/lint-eslint.yml new file mode 100644 index 0000000..5ad2d20 --- /dev/null +++ b/upstream/vendor/nextcloud/lint-eslint.yml @@ -0,0 +1,100 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Lint eslint + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-eslint-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'src/**' + - 'appinfo/info.xml' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + - '.eslintrc.*' + - '.eslintignore' + - '**.js' + - '**.ts' + - '**.vue' + + lint: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + name: NPM lint + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + with: + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Install dependencies + env: + CYPRESS_INSTALL_BINARY: 0 + PUPPETEER_SKIP_DOWNLOAD: true + run: npm ci + + - name: Lint + run: npm run lint + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, lint] + + if: always() + + # This is the summary, we just avoid to rename it so that branch protection rules still match + name: eslint + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi From e279e3b77940bddfcdcaeebf770e046e19b5562c Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:08 -0300 Subject: [PATCH 14/49] feat: move workflow catalog source data --- patches/nextcloud/lint-eslint.yml.patch | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 patches/nextcloud/lint-eslint.yml.patch diff --git a/patches/nextcloud/lint-eslint.yml.patch b/patches/nextcloud/lint-eslint.yml.patch new file mode 100644 index 0000000..ee9c343 --- /dev/null +++ b/patches/nextcloud/lint-eslint.yml.patch @@ -0,0 +1,28 @@ +--- upstream/vendor/nextcloud/lint-eslint.yml ++++ workflow-templates/lint-eslint.yml +@@ -1,6 +1,6 @@ + # 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: 2021-2024 Nextcloud GmbH and Nextcloud contributors +@@ -19,7 +19,7 @@ + + jobs: + changes: +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read +@@ -87,7 +87,7 @@ + summary: + permissions: + contents: none +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + needs: [changes, lint] + + if: always() From 313e165218eff7ca6ae6cd4248356131d99e9f9e Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:11 -0300 Subject: [PATCH 15/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/lint-info-xml.yml | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 upstream/vendor/nextcloud/lint-info-xml.yml diff --git a/upstream/vendor/nextcloud/lint-info-xml.yml b/upstream/vendor/nextcloud/lint-info-xml.yml new file mode 100644 index 0000000..8e6968c --- /dev/null +++ b/upstream/vendor/nextcloud/lint-info-xml.yml @@ -0,0 +1,40 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Lint info.xml + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-info-xml-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + xml-linters: + runs-on: ubuntu-latest-low + + name: info.xml lint + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + sparse-checkout: | + appinfo/ + + - name: Download schema + run: wget https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd + + - name: Lint info.xml + uses: ChristophWurst/xmllint-action@36f2a302f84f8c83fceea0b9c59e1eb4a616d3c1 # v1.2 + with: + xml-file: ./appinfo/info.xml + xml-schema-file: ./info.xsd From 220bc425c0d6e14325596bbdacda2e269e570524 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:13 -0300 Subject: [PATCH 16/49] feat: move workflow catalog source data --- patches/nextcloud/lint-info-xml.yml.patch | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 patches/nextcloud/lint-info-xml.yml.patch diff --git a/patches/nextcloud/lint-info-xml.yml.patch b/patches/nextcloud/lint-info-xml.yml.patch new file mode 100644 index 0000000..bb9ec28 --- /dev/null +++ b/patches/nextcloud/lint-info-xml.yml.patch @@ -0,0 +1,19 @@ +--- upstream/vendor/nextcloud/lint-info-xml.yml ++++ workflow-templates/lint-info-xml.yml +@@ -1,6 +1,6 @@ + # 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: 2021-2024 Nextcloud GmbH and Nextcloud contributors +@@ -19,7 +19,7 @@ + + jobs: + xml-linters: +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + + name: info.xml lint + steps: From 3dd8c7fee8b7969bcbe841c62495ae899072ba76 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:16 -0300 Subject: [PATCH 17/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/lint-php.yml | 76 ++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 upstream/vendor/nextcloud/lint-php.yml diff --git a/upstream/vendor/nextcloud/lint-php.yml b/upstream/vendor/nextcloud/lint-php.yml new file mode 100644 index 0000000..e4c6e9b --- /dev/null +++ b/upstream/vendor/nextcloud/lint-php.yml @@ -0,0 +1,76 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Lint php + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-php-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + matrix: + runs-on: ubuntu-latest-low + outputs: + php-min: ${{ steps.versions.outputs.php-min }} + php-max: ${{ steps.versions.outputs.php-max }} + steps: + - name: Checkout app + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Get version matrix + id: versions + uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 + + php-lint: + runs-on: ubuntu-latest-low + needs: matrix + strategy: + matrix: + php-versions: ['${{ needs.matrix.outputs.php-min }}', '${{ needs.matrix.outputs.php-max }}'] + + name: php-lint + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ matrix.php-versions }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Lint + run: composer run lint + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: php-lint + + if: always() + + name: php-lint-summary + + steps: + - name: Summary status + run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi From 47d716a0c12e0c820f36dbfb7611e20db1111f92 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:19 -0300 Subject: [PATCH 18/49] feat: move workflow catalog source data --- patches/nextcloud/lint-php.yml.patch | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 patches/nextcloud/lint-php.yml.patch diff --git a/patches/nextcloud/lint-php.yml.patch b/patches/nextcloud/lint-php.yml.patch new file mode 100644 index 0000000..0d60065 --- /dev/null +++ b/patches/nextcloud/lint-php.yml.patch @@ -0,0 +1,37 @@ +--- upstream/vendor/nextcloud/lint-php.yml ++++ workflow-templates/lint-php.yml +@@ -1,6 +1,6 @@ + # 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: 2021-2024 Nextcloud GmbH and Nextcloud contributors +@@ -19,7 +19,7 @@ + + jobs: + matrix: +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + outputs: + php-min: ${{ steps.versions.outputs.php-min }} + php-max: ${{ steps.versions.outputs.php-max }} +@@ -34,7 +34,7 @@ + uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 + + php-lint: +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + needs: matrix + strategy: + matrix: +@@ -64,7 +64,7 @@ + summary: + permissions: + contents: none +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + needs: php-lint + + if: always() From 3c6661b3ca79bcb6d0c1b1a7ea4ce6e71d0aa414 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:26 -0300 Subject: [PATCH 19/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/lint-php-cs.yml | 54 +++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 upstream/vendor/nextcloud/lint-php-cs.yml diff --git a/upstream/vendor/nextcloud/lint-php-cs.yml b/upstream/vendor/nextcloud/lint-php-cs.yml new file mode 100644 index 0000000..06d0842 --- /dev/null +++ b/upstream/vendor/nextcloud/lint-php-cs.yml @@ -0,0 +1,54 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Lint php-cs + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-php-cs-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + + name: php-cs + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Get php version + id: versions + uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 + + - name: Set up php${{ steps.versions.outputs.php-min }} + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ steps.versions.outputs.php-min }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Remove nextcloud/ocp + run: | + composer remove nextcloud/ocp --dev --no-scripts + + - name: Install composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + + - name: Lint + run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) From 275a3cbc65ba21d20ce0b08a21c861b6739ebe47 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:29 -0300 Subject: [PATCH 20/49] feat: move workflow catalog source data --- patches/nextcloud/lint-php-cs.yml.patch | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 patches/nextcloud/lint-php-cs.yml.patch diff --git a/patches/nextcloud/lint-php-cs.yml.patch b/patches/nextcloud/lint-php-cs.yml.patch new file mode 100644 index 0000000..23726f9 --- /dev/null +++ b/patches/nextcloud/lint-php-cs.yml.patch @@ -0,0 +1,10 @@ +--- upstream/vendor/nextcloud/lint-php-cs.yml ++++ workflow-templates/lint-php-cs.yml +@@ -1,6 +1,6 @@ + # 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: 2021-2024 Nextcloud GmbH and Nextcloud contributors From 1009a92a04061e734f4c532b994c0dca53c1fdd3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:31 -0300 Subject: [PATCH 21/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/lint-stylelint.yml | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 upstream/vendor/nextcloud/lint-stylelint.yml diff --git a/upstream/vendor/nextcloud/lint-stylelint.yml b/upstream/vendor/nextcloud/lint-stylelint.yml new file mode 100644 index 0000000..e4b26b5 --- /dev/null +++ b/upstream/vendor/nextcloud/lint-stylelint.yml @@ -0,0 +1,53 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Lint stylelint + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-stylelint-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + + name: stylelint + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + with: + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Install dependencies + env: + CYPRESS_INSTALL_BINARY: 0 + run: npm ci + + - name: Lint + run: npm run stylelint From 82866d7517f6a5ad46cdc70c1fa8f1b4cd763705 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:34 -0300 Subject: [PATCH 22/49] feat: move workflow catalog source data --- patches/nextcloud/lint-stylelint.yml.patch | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 patches/nextcloud/lint-stylelint.yml.patch diff --git a/patches/nextcloud/lint-stylelint.yml.patch b/patches/nextcloud/lint-stylelint.yml.patch new file mode 100644 index 0000000..3ce1d8c --- /dev/null +++ b/patches/nextcloud/lint-stylelint.yml.patch @@ -0,0 +1,10 @@ +--- upstream/vendor/nextcloud/lint-stylelint.yml ++++ workflow-templates/lint-stylelint.yml +@@ -1,6 +1,6 @@ + # 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: 2021-2024 Nextcloud GmbH and Nextcloud contributors From e3132751f7b9ed2482cce4c076e413d0248aa122 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:37 -0300 Subject: [PATCH 23/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/lint-typescript.yml | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 upstream/vendor/nextcloud/lint-typescript.yml diff --git a/upstream/vendor/nextcloud/lint-typescript.yml b/upstream/vendor/nextcloud/lint-typescript.yml new file mode 100644 index 0000000..0b865e6 --- /dev/null +++ b/upstream/vendor/nextcloud/lint-typescript.yml @@ -0,0 +1,100 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Type checking + +on: + pull_request: + push: + branches: + - main + - master + - stable* + +permissions: + contents: read + +concurrency: + group: lint-typescript-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/lint-typescript.yml' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + - '**.ts' + - '**.vue' + + test: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + with: + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Install dependencies + env: + CYPRESS_INSTALL_BINARY: 0 + run: | + npm ci + + - name: Check types + run: | + npm run --if-present check-types + npm run --if-present ts:check + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, test] + + if: always() + + name: typescript-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.test.result != 'success' }}; then exit 1; fi From 7a0a2ff6c7e54c03431b10d21eccbd06876ea805 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:40 -0300 Subject: [PATCH 24/49] feat: move workflow catalog source data --- patches/nextcloud/lint-typescript.yml.patch | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 patches/nextcloud/lint-typescript.yml.patch diff --git a/patches/nextcloud/lint-typescript.yml.patch b/patches/nextcloud/lint-typescript.yml.patch new file mode 100644 index 0000000..c262230 --- /dev/null +++ b/patches/nextcloud/lint-typescript.yml.patch @@ -0,0 +1,28 @@ +--- upstream/vendor/nextcloud/lint-typescript.yml ++++ workflow-templates/lint-typescript.yml +@@ -1,6 +1,6 @@ + # 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: 2023-2024 Nextcloud GmbH and Nextcloud contributors +@@ -25,7 +25,7 @@ + + jobs: + changes: +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read +@@ -88,7 +88,7 @@ + summary: + permissions: + contents: none +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + needs: [changes, test] + + if: always() From b5a01345a92909044f3171e905b27f14d573afad Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:43 -0300 Subject: [PATCH 25/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/node-test.yml | 112 ++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 upstream/vendor/nextcloud/node-test.yml diff --git a/upstream/vendor/nextcloud/node-test.yml b/upstream/vendor/nextcloud/node-test.yml new file mode 100644 index 0000000..1b74d3e --- /dev/null +++ b/upstream/vendor/nextcloud/node-test.yml @@ -0,0 +1,112 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Node tests + +on: + pull_request: + push: + branches: + - main + - master + - stable* + +permissions: + contents: read + +concurrency: + group: node-tests-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - '__tests__/**' + - '__mocks__/**' + - 'src/**' + - 'appinfo/info.xml' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + - '**.js' + - '**.ts' + - '**.vue' + + test: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + with: + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Install dependencies & build + env: + CYPRESS_INSTALL_BINARY: 0 + run: | + npm ci + npm run build --if-present + + - name: Test + run: npm run test --if-present + + - name: Test and process coverage + run: npm run test:coverage --if-present + + - name: Collect coverage + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + files: ./coverage/lcov.info + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, test] + + if: always() + + name: test-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.test.result != 'success' }}; then exit 1; fi From 86a0d0779924dce6dfb85991898a42245017c0b8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:45 -0300 Subject: [PATCH 26/49] feat: move workflow catalog source data --- patches/nextcloud/node-test.yml.patch | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 patches/nextcloud/node-test.yml.patch diff --git a/patches/nextcloud/node-test.yml.patch b/patches/nextcloud/node-test.yml.patch new file mode 100644 index 0000000..5ec726f --- /dev/null +++ b/patches/nextcloud/node-test.yml.patch @@ -0,0 +1,28 @@ +--- upstream/vendor/nextcloud/node-test.yml ++++ workflow-templates/node-test.yml +@@ -1,6 +1,6 @@ + # 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: 2023-2024 Nextcloud GmbH and Nextcloud contributors +@@ -25,7 +25,7 @@ + + jobs: + changes: +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read +@@ -100,7 +100,7 @@ + summary: + permissions: + contents: none +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + needs: [changes, test] + + if: always() From 40f994b45e3b75085f75284931898a5103e89ea2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:48 -0300 Subject: [PATCH 27/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/npm-build.yml | 113 ++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 upstream/vendor/nextcloud/npm-build.yml diff --git a/upstream/vendor/nextcloud/npm-build.yml b/upstream/vendor/nextcloud/npm-build.yml new file mode 100644 index 0000000..c608572 --- /dev/null +++ b/upstream/vendor/nextcloud/npm-build.yml @@ -0,0 +1,113 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Build Javascript + +on: pull_request + +permissions: + contents: read + +concurrency: + group: node-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'src/**' + - 'appinfo/info.xml' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + - '**.js' + - '**.ts' + - '**.vue' + + build: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + name: NPM build + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + with: + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Validate package-lock.json # See https://github.com/npm/cli/issues/4460 + run: | + npm i -g npm-package-lock-add-resolved@1.1.4 + npm-package-lock-add-resolved + git --no-pager diff --exit-code + + - name: Install dependencies & build + env: + CYPRESS_INSTALL_BINARY: 0 + PUPPETEER_SKIP_DOWNLOAD: true + run: | + npm ci + npm run build --if-present + + - name: Check build changes + run: | + bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please recompile and commit the assets, see the section \"Show changes on failure\" for details' && exit 1)" + + - name: Show changes on failure + if: failure() + run: | + git status + git --no-pager diff + exit 1 # make it red to grab attention + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, build] + + if: always() + + # This is the summary, we just avoid to rename it so that branch protection rules still match + name: node + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.build.result != 'success' }}; then exit 1; fi From 1e02ac9869194cbc0f28ca2e9f30fec6f4ac4de8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:51 -0300 Subject: [PATCH 28/49] feat: move workflow catalog source data --- patches/nextcloud/npm-build.yml.patch | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 patches/nextcloud/npm-build.yml.patch diff --git a/patches/nextcloud/npm-build.yml.patch b/patches/nextcloud/npm-build.yml.patch new file mode 100644 index 0000000..ba03e7e --- /dev/null +++ b/patches/nextcloud/npm-build.yml.patch @@ -0,0 +1,28 @@ +--- upstream/vendor/nextcloud/npm-build.yml ++++ workflow-templates/npm-build.yml +@@ -1,6 +1,6 @@ + # 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: 2021-2024 Nextcloud GmbH and Nextcloud contributors +@@ -19,7 +19,7 @@ + + jobs: + changes: +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read +@@ -100,7 +100,7 @@ + summary: + permissions: + contents: none +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + needs: [changes, build] + + if: always() From 83a7f31561d27f402edec91ee659c79319a0f8aa Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:50:57 -0300 Subject: [PATCH 29/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/openapi.yml | 96 +++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 upstream/vendor/nextcloud/openapi.yml diff --git a/upstream/vendor/nextcloud/openapi.yml b/upstream/vendor/nextcloud/openapi.yml new file mode 100644 index 0000000..7485656 --- /dev/null +++ b/upstream/vendor/nextcloud/openapi.yml @@ -0,0 +1,96 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2024 Arthur Schiwon +# SPDX-License-Identifier: MIT + +name: OpenAPI + +on: pull_request + +permissions: + contents: read + +concurrency: + group: openapi-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + openapi: + runs-on: ubuntu-latest + + if: ${{ github.repository_owner != 'nextcloud-gmbh' }} + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Get php version + id: php_versions + uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 + + - name: Set up php + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ steps.php_versions.outputs.php-available }} + extensions: xml + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check Typescript OpenApi types + id: check_typescript_openapi + uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0 + with: + files: "src/types/openapi/openapi*.ts" + + - name: Read package.json node and npm engines version + if: steps.check_typescript_openapi.outputs.files_exists == 'true' + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: node_versions + # Continue if no package.json + continue-on-error: true + with: + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.node_versions.outputs.nodeVersion }} + if: ${{ steps.node_versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.node_versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.node_versions.outputs.npmVersion }} + if: ${{ steps.node_versions.outputs.nodeVersion }} + run: npm i -g 'npm@${{ steps.node_versions.outputs.npmVersion }}' + + - name: Install dependencies + if: ${{ steps.node_versions.outputs.nodeVersion }} + env: + CYPRESS_INSTALL_BINARY: 0 + PUPPETEER_SKIP_DOWNLOAD: true + run: | + npm ci + + - name: Install composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + + - name: Regenerate OpenAPI + run: composer run openapi + + - name: Check openapi*.json and typescript changes + run: | + bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please run \"composer run openapi\" and commit the openapi*.json files and (if applicable) src/types/openapi/openapi*.ts, see the section \"Show changes on failure\" for details' && exit 1)" + + - name: Show changes on failure + if: failure() + run: | + git status + git --no-pager diff + exit 1 # make it red to grab attention From d33bc8595c9197b9bfe0694d56b52e3e5298abef Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:00 -0300 Subject: [PATCH 30/49] feat: move workflow catalog source data --- patches/nextcloud/openapi.yml.patch | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 patches/nextcloud/openapi.yml.patch diff --git a/patches/nextcloud/openapi.yml.patch b/patches/nextcloud/openapi.yml.patch new file mode 100644 index 0000000..2fb3ad6 --- /dev/null +++ b/patches/nextcloud/openapi.yml.patch @@ -0,0 +1,10 @@ +--- upstream/vendor/nextcloud/openapi.yml ++++ workflow-templates/openapi.yml +@@ -1,6 +1,6 @@ + # 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: 2024 Nextcloud GmbH and Nextcloud contributors From 8959ccd188b03be6dc14f8be1a27dc87db805789 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:03 -0300 Subject: [PATCH 31/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/psalm.yml | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 upstream/vendor/nextcloud/psalm.yml diff --git a/upstream/vendor/nextcloud/psalm.yml b/upstream/vendor/nextcloud/psalm.yml new file mode 100644 index 0000000..d6e2d52 --- /dev/null +++ b/upstream/vendor/nextcloud/psalm.yml @@ -0,0 +1,61 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Static analysis + +on: pull_request + +concurrency: + group: psalm-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + static-analysis: + runs-on: ubuntu-latest + + name: static-psalm-analysis + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Get php version + id: versions + uses: nextcloud-libraries/nextcloud-version-matrix@cd0211ffcef1065e2020cd579e4843b8746e7a58 # v1.3.3 + + - name: Check enforcement of minimum PHP version ${{ steps.versions.outputs.php-min }} in psalm.xml + run: grep 'phpVersion="${{ steps.versions.outputs.php-min }}' psalm.xml + + - name: Set up php${{ steps.versions.outputs.php-available }} + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ steps.versions.outputs.php-available }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite + coverage: none + ini-file: development + # Temporary workaround for missing pcntl_* in PHP 8.3 + ini-values: disable_functions= + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Remove nextcloud/ocp + run: | + composer remove nextcloud/ocp --dev --no-scripts + + - name: Install composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + + - name: Install nextcloud/ocp:dev-${{ steps.versions.outputs.branches-max }} + run: composer require --dev nextcloud/ocp:dev-${{ steps.versions.outputs.branches-max }} --ignore-platform-reqs --with-dependencies + + - name: Run coding standards check + run: composer run psalm -- --threads=1 --monochrome --no-progress --output-format=github From cf5c1cb9874e7349680d2ca85fb1b05a871079ed Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:06 -0300 Subject: [PATCH 32/49] feat: move workflow catalog source data --- patches/nextcloud/psalm.yml.patch | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 patches/nextcloud/psalm.yml.patch diff --git a/patches/nextcloud/psalm.yml.patch b/patches/nextcloud/psalm.yml.patch new file mode 100644 index 0000000..ca1bf08 --- /dev/null +++ b/patches/nextcloud/psalm.yml.patch @@ -0,0 +1,10 @@ +--- upstream/vendor/nextcloud/psalm.yml ++++ workflow-templates/psalm.yml +@@ -1,6 +1,6 @@ + # 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: 2022-2024 Nextcloud GmbH and Nextcloud contributors From 741828e7bccb82b4c0a8b31fcc18f9f410fac968 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:08 -0300 Subject: [PATCH 33/49] feat: move workflow catalog source data --- upstream/vendor/nextcloud/reuse.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 upstream/vendor/nextcloud/reuse.yml diff --git a/upstream/vendor/nextcloud/reuse.yml b/upstream/vendor/nextcloud/reuse.yml new file mode 100644 index 0000000..67fdafc --- /dev/null +++ b/upstream/vendor/nextcloud/reuse.yml @@ -0,0 +1,27 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization + +# SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. +# +# SPDX-License-Identifier: CC0-1.0 + +name: REUSE Compliance Check + +on: [pull_request] + +permissions: + contents: read + +jobs: + reuse-compliance-check: + runs-on: ubuntu-latest-low + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: REUSE Compliance Check + uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 From b8bc1960929b5cdae44167776e934fcc4acab556 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:11 -0300 Subject: [PATCH 34/49] feat: move workflow catalog source data --- patches/nextcloud/reuse.yml.patch | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 patches/nextcloud/reuse.yml.patch diff --git a/patches/nextcloud/reuse.yml.patch b/patches/nextcloud/reuse.yml.patch new file mode 100644 index 0000000..6d6bea5 --- /dev/null +++ b/patches/nextcloud/reuse.yml.patch @@ -0,0 +1,19 @@ +--- upstream/vendor/nextcloud/reuse.yml ++++ workflow-templates/reuse.yml +@@ -1,6 +1,6 @@ + # 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: 2022 Free Software Foundation Europe e.V. +@@ -16,7 +16,7 @@ + + jobs: + reuse-compliance-check: +- runs-on: ubuntu-latest-low ++ runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 From 3d5c5644699d333a7599d81b28815d30f4bce776 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:14 -0300 Subject: [PATCH 35/49] feat: move workflow catalog source data --- .../nextcloud/sync-workflow-templates.yml | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 upstream/vendor/nextcloud/sync-workflow-templates.yml diff --git a/upstream/vendor/nextcloud/sync-workflow-templates.yml b/upstream/vendor/nextcloud/sync-workflow-templates.yml new file mode 100644 index 0000000..40d39e1 --- /dev/null +++ b/upstream/vendor/nextcloud/sync-workflow-templates.yml @@ -0,0 +1,141 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# 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 +name: Update workflows +on: + workflow_dispatch: + schedule: + - cron: "5 2 * * 0" + +permissions: + contents: read + +jobs: + dispatch: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + branches: + - ${{ github.event.repository.default_branch }} + - 'stable35' + - 'stable34' + - 'stable33' + + name: Update workflows in ${{ matrix.branches }} + + permissions: + contents: write + pull-requests: write + + steps: + - name: Check actor permission + uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 + with: + require: admin + + - name: Checkout workflow repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + path: source + repository: nextcloud/.github + + - name: Checkout app + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + path: target + ref: ${{ matrix.branches }} + + - name: Copy all workflow templates + run: | + echo 'SUMMARY<> $GITHUB_ENV + draft_only=0 + for workflow in ./source/workflow-templates/*.yml; do + echo "❓ Looking for $workflow" + if [ -f "$workflow" ]; then + filename=$(basename "$workflow") + target_file="./target/.github/workflows/$filename" + + # Only copy if the file exists in the target repository + if [ -f "$target_file" ]; then + if [ -f "./target/.github/actions-lock.txt" ]; then + locked_version=$(grep " $filename" ./target/.github/actions-lock.txt | cat) + else + echo "# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors" >> ./target/.github/actions-lock.txt + echo "# SPDX-License""-Identifier: MIT" >> ./target/.github/actions-lock.txt + locked_version="" + fi + locked_version=$(echo $locked_version | cut -f 1 -d " ") + new_version=$(md5sum $workflow | cut -f 1 -d " ") + + # Only update if the action changes + if [[ "$locked_version" != "$new_version" ]]; then + echo "ℹ️ Locked version: $locked_version" + echo "ℹ️ Current version: $new_version" + echo "🆙 Updating existing workflow: $filename" + echo "- 🆙 Updated [$filename](https://github.com/nextcloud/.github/commits/master/workflow-templates/$filename)" >> $GITHUB_ENV + + cp "$workflow" "$target_file" + + # Apply patch if one exists + if [ -f "$target_file.patch" ]; then + echo "🩹 Applying patch" + cd ./target + set +e + patch -p1 < ".github/workflows/$filename.patch" + patch_worked=$? + set -e + cd - + if [[ "$patch_worked" == "0" ]]; then + echo " - Patch applied" >> $GITHUB_ENV + else + echo " - [ ] ❌ Patch failed" >> $GITHUB_ENV + draft_only=1 + fi + fi + + if [[ "$locked_version" != "" ]]; then + sed -i "s/$locked_version $filename/$new_version $filename/" ./target/.github/actions-lock.txt + else + echo "$new_version $filename" >> ./target/.github/actions-lock.txt + fi + else + echo "✅ Skipping $filename: already up to date" + fi + else + echo "⏭️ Skipping $filename: does not exist in target repository" + fi + fi + done + echo 'EOF' >> $GITHUB_ENV + echo "DRAFT_ONLY=${draft_only}" >> $GITHUB_ENV + + - name: Create Pull Request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.COMMAND_BOT_WORKFLOWS }} + commit-message: 'ci(actions): Update workflow templates from organization template repository' + committer: GitHub + author: nextcloud-command + path: target + signoff: true + branch: 'automated/noid/${{ matrix.branches }}-update-workflows' + title: '[${{ matrix.branches }}] ci(actions): Update workflow templates from organization template repository' + draft: ${{ env.DRAFT_ONLY == 1 }} + add-paths: .github/workflows/*.yml,.github/actions-lock.txt + body: | + Automated update of all workflow templates from [nextcloud/.github](https://github.com/nextcloud/.github) + ${{ env.SUMMARY }} + labels: | + dependencies + 3. to review From 55af69a26280fdacf7409a906e7c22ab72287462 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:18 -0300 Subject: [PATCH 36/49] feat: move workflow catalog source data --- .../sync-workflow-templates.yml.patch | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 patches/nextcloud/sync-workflow-templates.yml.patch diff --git a/patches/nextcloud/sync-workflow-templates.yml.patch b/patches/nextcloud/sync-workflow-templates.yml.patch new file mode 100644 index 0000000..f3ba378 --- /dev/null +++ b/patches/nextcloud/sync-workflow-templates.yml.patch @@ -0,0 +1,247 @@ +--- upstream/vendor/nextcloud/sync-workflow-templates.yml ++++ workflow-templates/sync-workflow-templates.yml +@@ -1,13 +1,16 @@ + # 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 + # 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 selected explicitly with vars.WORKFLOW_SYNC_AUTH_MODE. ++# See docs/cross-repository-automation.md for modes, credentials and GITHUB_TOKEN limitations. + name: Update workflows + on: + workflow_dispatch: +@@ -26,9 +29,6 @@ + matrix: + branches: + - ${{ github.event.repository.default_branch }} +- - 'stable35' +- - 'stable34' +- - 'stable33' + + name: Update workflows in ${{ matrix.branches }} + +@@ -42,12 +42,111 @@ + with: + require: admin + ++ - 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@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 ++ with: ++ app-id: ${{ vars.LIBRECODE_WORKFLOW_APP_ID }} ++ private-key: ${{ secrets.LIBRECODE_WORKFLOW_APP_PRIVATE_KEY }} ++ owner: ${{ github.repository_owner }} ++ repositories: ${{ github.event.repository.name }} ++ 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@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 ++ 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: + persist-credentials: false + path: source +- repository: nextcloud/.github ++ repository: LibreCodeCoop/.github ++ ++ - name: Record workflow catalog revision ++ id: catalog-revision ++ working-directory: source ++ shell: bash ++ run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Checkout app + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 +@@ -56,86 +155,33 @@ + path: target + ref: ${{ matrix.branches }} + +- - name: Copy all workflow templates +- run: | +- echo 'SUMMARY<> $GITHUB_ENV +- draft_only=0 +- for workflow in ./source/workflow-templates/*.yml; do +- echo "❓ Looking for $workflow" +- if [ -f "$workflow" ]; then +- filename=$(basename "$workflow") +- target_file="./target/.github/workflows/$filename" +- +- # Only copy if the file exists in the target repository +- if [ -f "$target_file" ]; then +- if [ -f "./target/.github/actions-lock.txt" ]; then +- locked_version=$(grep " $filename" ./target/.github/actions-lock.txt | cat) +- else +- echo "# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors" >> ./target/.github/actions-lock.txt +- echo "# SPDX-License""-Identifier: MIT" >> ./target/.github/actions-lock.txt +- locked_version="" +- fi +- locked_version=$(echo $locked_version | cut -f 1 -d " ") +- new_version=$(md5sum $workflow | cut -f 1 -d " ") +- +- # Only update if the action changes +- if [[ "$locked_version" != "$new_version" ]]; then +- echo "ℹ️ Locked version: $locked_version" +- echo "ℹ️ Current version: $new_version" +- echo "🆙 Updating existing workflow: $filename" +- echo "- 🆙 Updated [$filename](https://github.com/nextcloud/.github/commits/master/workflow-templates/$filename)" >> $GITHUB_ENV +- +- cp "$workflow" "$target_file" +- +- # Apply patch if one exists +- if [ -f "$target_file.patch" ]; then +- echo "🩹 Applying patch" +- cd ./target +- set +e +- patch -p1 < ".github/workflows/$filename.patch" +- patch_worked=$? +- set -e +- cd - +- if [[ "$patch_worked" == "0" ]]; then +- echo " - Patch applied" >> $GITHUB_ENV +- else +- echo " - [ ] ❌ Patch failed" >> $GITHUB_ENV +- draft_only=1 +- fi +- fi +- +- if [[ "$locked_version" != "" ]]; then +- sed -i "s/$locked_version $filename/$new_version $filename/" ./target/.github/actions-lock.txt +- else +- echo "$new_version $filename" >> ./target/.github/actions-lock.txt +- fi +- else +- echo "✅ Skipping $filename: already up to date" +- fi +- else +- echo "⏭️ Skipping $filename: does not exist in target repository" +- fi +- fi +- done +- echo 'EOF' >> $GITHUB_ENV +- echo "DRAFT_ONLY=${draft_only}" >> $GITHUB_ENV ++ - name: Synchronize workflow templates ++ id: sync ++ uses: LibreCodeCoop/github-workflows/actions/sync-workflows@002f17274ba1eade3351ba81890bf53674b37c43 # v0.4.0 ++ with: ++ source: source/workflow-templates ++ target: target ++ platform-version: v0.4.0 ++ source-commit: 002f17274ba1eade3351ba81890bf53674b37c43 ++ catalog-commit: ${{ steps.catalog-revision.outputs.sha }} + + - name: Create Pull Request ++ if: ${{ steps.sync.outputs.changed == 'true' }} + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: +- token: ${{ secrets.COMMAND_BOT_WORKFLOWS }} ++ token: ${{ steps.auth-token.outputs.token }} + commit-message: 'ci(actions): Update workflow templates from organization template repository' + committer: GitHub +- author: nextcloud-command ++ 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: ${{ env.DRAFT_ONLY == 1 }} ++ draft: ${{ steps.sync.outputs.patch_failed == 'true' }} + add-paths: .github/workflows/*.yml,.github/actions-lock.txt + body: | +- Automated update of all workflow templates from [nextcloud/.github](https://github.com/nextcloud/.github) +- ${{ env.SUMMARY }} ++ Automated update of workflow templates from [LibreCodeCoop/.github](https://github.com/LibreCodeCoop/.github) ++ ${{ steps.sync.outputs.summary }} + labels: | + dependencies + 3. to review From 54edacd815d4c92d430a5662f908219816360c32 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:33 -0300 Subject: [PATCH 37/49] ci: validate workflow catalog at source --- .github/workflows/tests.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f21be76 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Python tests + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + tests: + name: Python tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run unit tests + run: python3 -m unittest discover -s tests -p 'test_*.py' + + - name: Verify vendored upstream sources + run: python3 scripts/sync_upstream.py check upstream/sources.json + + - name: Verify rendered upstream templates + run: python3 scripts/render_upstream.py check upstream/templates.json From f832b9a09d83f99715f754188d90310d7f29d059 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:35 -0300 Subject: [PATCH 38/49] ci: refresh upstream workflows directly in catalog --- .github/workflows/upstream-refresh.yml | 115 +++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 .github/workflows/upstream-refresh.yml diff --git a/.github/workflows/upstream-refresh.yml b/.github/workflows/upstream-refresh.yml new file mode 100644 index 0000000..359ff44 --- /dev/null +++ b/.github/workflows/upstream-refresh.yml @@ -0,0 +1,115 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Refresh upstream workflows + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'upstream/sources.json' + - 'upstream/templates.json' + - 'patches/nextcloud/**' + - '.github/workflows/upstream-refresh.yml' + schedule: + - cron: '17 3 * * 0' + +permissions: + contents: read + +jobs: + refresh: + name: Refresh pinned upstream sources + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - name: Validate GitHub App configuration + env: + WORKFLOW_APP_ID: ${{ vars.LIBRECODE_WORKFLOW_APP_ID }} + WORKFLOW_APP_PRIVATE_KEY: ${{ secrets.LIBRECODE_WORKFLOW_APP_PRIVATE_KEY }} + run: | + if [ -z "$WORKFLOW_APP_ID" ]; then + echo "::error::LIBRECODE_WORKFLOW_APP_ID is not configured." + exit 1 + fi + if [ -z "$WORKFLOW_APP_PRIVATE_KEY" ]; then + echo "::error::LIBRECODE_WORKFLOW_APP_PRIVATE_KEY is not configured." + exit 1 + fi + + - name: Create GitHub App token + id: app-token + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + with: + app-id: ${{ vars.LIBRECODE_WORKFLOW_APP_ID }} + private-key: ${{ secrets.LIBRECODE_WORKFLOW_APP_PRIVATE_KEY }} + owner: LibreCodeCoop + repositories: .github + permission-contents: write + permission-pull-requests: write + permission-workflows: write + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Refresh upstream pins + id: refresh + env: + GITHUB_TOKEN: ${{ github.token }} + run: python3 scripts/sync_upstream.py refresh upstream/sources.json + + - name: Verify refreshed immutable sources + run: python3 scripts/sync_upstream.py check upstream/sources.json + + - name: Apply downstream patches + id: render + continue-on-error: true + run: | + python3 scripts/render_upstream.py sync upstream/templates.json \ + --report render-report.json + + - name: Run unit tests + if: always() + run: python3 -m unittest discover -s tests -p 'test_*.py' + + - name: Build pull request report + if: steps.refresh.outcome == 'success' + run: >- + python3 scripts/render_upstream.py pr-body + --report render-report.json + --output refresh-upstream-pr.md + + - name: Create update pull request + id: pull-request + if: steps.refresh.outcome == 'success' + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: 'chore: refresh upstream workflow pins' + committer: GitHub + author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + signoff: true + branch: automated/refresh-upstream-workflows + delete-branch: true + title: 'chore: refresh upstream workflows' + body-path: refresh-upstream-pr.md + draft: ${{ steps.render.outcome == 'failure' }} + labels: dependencies + add-paths: | + upstream/sources.json + upstream/vendor/** + workflow-templates/** + .github/workflows/** + + - name: Fail when patches need manual updates + if: steps.render.outcome == 'failure' + run: | + echo "One or more downstream patches could not be applied." + exit 1 From 0b14ed00eaf4cef007a12f7124f3efefb94e3555 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:51:41 -0300 Subject: [PATCH 39/49] ci: let Dependabot maintain workflow templates --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 94f8369..4a6840e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,3 +16,17 @@ updates: update-types: - minor - patch + + - package-ecosystem: github-actions + directory: /workflow-templates + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + workflow-template-actions: + patterns: + - "*" + update-types: + - minor + - patch From bfbd462b27eb69f0f092d9c7ac3e82c2ab8255be Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:52:12 -0300 Subject: [PATCH 40/49] ci: move catalog validation workflows --- .github/workflows/workflow-policy.yml | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/workflow-policy.yml diff --git a/.github/workflows/workflow-policy.yml b/.github/workflows/workflow-policy.yml new file mode 100644 index 0000000..903dd17 --- /dev/null +++ b/.github/workflows/workflow-policy.yml @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Workflow policy + +on: + pull_request: + paths: + - '.github/workflows/**' + - 'workflow-templates/**' + - 'scripts/check_workflow_policy.py' + - 'tests/test_workflow_policy.py' + push: + branches: + - main + paths: + - '.github/workflows/**' + - 'workflow-templates/**' + - 'scripts/check_workflow_policy.py' + - 'tests/test_workflow_policy.py' + +permissions: + contents: read + +jobs: + policy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Check workflow policy + run: python3 scripts/check_workflow_policy.py From bd144ac7c357024aa16e595a08695e4841578910 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:52:15 -0300 Subject: [PATCH 41/49] ci: move catalog validation workflows --- .github/workflows/reuse.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/reuse.yml diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml new file mode 100644 index 0000000..eaf21f1 --- /dev/null +++ b/.github/workflows/reuse.yml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: REUSE Compliance Check + +on: + workflow_call: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + reuse: + name: REUSE Compliance Check + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run REUSE + uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 From 17dd032c2bffd25186f64fc5cb80798b62ed9ce0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:53:59 -0300 Subject: [PATCH 42/49] feat: preserve template action pins during renders --- scripts/render_upstream.py | 42 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/scripts/render_upstream.py b/scripts/render_upstream.py index 60e1bbc..6a0604b 100644 --- a/scripts/render_upstream.py +++ b/scripts/render_upstream.py @@ -6,6 +6,7 @@ import argparse import json +import re import shutil import subprocess import tempfile @@ -13,6 +14,12 @@ from pathlib import Path +ACTION_USE_RE = re.compile( + r"^(?P\s*uses:\s*)(?P[^@\s]+)@(?P[0-9a-f]{40})(?P\s*(?:#.*)?)$", + re.MULTILINE, +) + + @dataclass(frozen=True) class Template: name: str @@ -89,14 +96,39 @@ def render(template: Template, root: Path) -> bytes: return working.read_bytes() +def preserve_action_pins(rendered: bytes, current: bytes | None) -> bytes: + if current is None: + return rendered + + rendered_text = rendered.decode("utf-8") + current_text = current.decode("utf-8") + + current_uses: dict[str, list[str]] = {} + for match in ACTION_USE_RE.finditer(current_text): + current_uses.setdefault(match.group("action"), []).append(match.group(0)) + + offsets: dict[str, int] = {} + + def replace(match: re.Match[str]) -> str: + action = match.group("action") + index = offsets.get(action, 0) + offsets[action] = index + 1 + candidates = current_uses.get(action, []) + if index >= len(candidates): + return match.group(0) + return candidates[index] + + return ACTION_USE_RE.sub(replace, rendered_text).encode("utf-8") + + def sync(templates: list[Template], root: Path) -> dict[str, object]: results: list[dict[str, object]] = [] for template in templates: destination = _safe_path(root, template.destination) try: - content = render(template, root) previous = destination.read_bytes() if destination.is_file() else None + content = preserve_action_pins(render(template, root), previous) changed = previous != content destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(content) @@ -140,7 +172,13 @@ def check(templates: list[Template], root: Path) -> None: continue destination = _safe_path(root, template.destination) - if not destination.is_file() or destination.read_bytes() != expected: + if not destination.is_file(): + problems.append(f"{template.name}: rendered template is out of date") + continue + + current = destination.read_bytes() + expected = preserve_action_pins(expected, current) + if current != expected: problems.append(f"{template.name}: rendered template is out of date") if problems: From 925795c6e381bfd74e3df7a38e8fc1030758bbbe Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:54:11 -0300 Subject: [PATCH 43/49] test: keep Dependabot pins authoritative --- tests/test_render_upstream.py | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/test_render_upstream.py b/tests/test_render_upstream.py index 5234f05..50c179e 100644 --- a/tests/test_render_upstream.py +++ b/tests/test_render_upstream.py @@ -70,6 +70,82 @@ def test_sync_applies_patch(self) -> None: self.assertEqual(report["updated"], 1) self.assertEqual(report["failed"], 0) + def test_sync_preserves_dependabot_action_pin(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root, manifest = self.fixture(directory) + source = root / "upstream/vendor/example.yml" + source.write_text( + "name: Example\n\njobs:\n test:\n uses: actions/checkout@" + + "1" * 40 + + " # v1\n", + encoding="utf-8", + ) + manifest.write_text( + json.dumps( + { + "templates": [ + { + "name": "example", + "source": "upstream/vendor/example.yml", + "patches": [], + "destination": "workflow-templates/example.yml", + } + ] + } + ), + encoding="utf-8", + ) + destination = root / "workflow-templates/example.yml" + destination.parent.mkdir(parents=True) + destination.write_text( + "name: Example\n\njobs:\n test:\n uses: actions/checkout@" + + "2" * 40 + + " # v2\n", + encoding="utf-8", + ) + + report = sync(load_templates(manifest), root) + + self.assertTrue(report["ok"]) + self.assertEqual(report["unchanged"], 1) + self.assertIn("actions/checkout@" + "2" * 40, destination.read_text()) + + def test_check_accepts_only_action_pin_drift(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root, manifest = self.fixture(directory) + source = root / "upstream/vendor/example.yml" + source.write_text( + "name: Example\n\njobs:\n test:\n uses: actions/checkout@" + + "1" * 40 + + " # v1\n", + encoding="utf-8", + ) + manifest.write_text( + json.dumps( + { + "templates": [ + { + "name": "example", + "source": "upstream/vendor/example.yml", + "patches": [], + "destination": "workflow-templates/example.yml", + } + ] + } + ), + encoding="utf-8", + ) + destination = root / "workflow-templates/example.yml" + destination.parent.mkdir(parents=True) + destination.write_text( + "name: Example\n\njobs:\n test:\n uses: actions/checkout@" + + "2" * 40 + + " # v2\n", + encoding="utf-8", + ) + + check(load_templates(manifest), root) + def test_sync_reports_failure_and_continues(self) -> None: with tempfile.TemporaryDirectory() as directory: root, manifest = self.fixture(directory) From 3f752f63f25cb2edc8f5b37d474d9c503c7b0c56 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:54:40 -0300 Subject: [PATCH 44/49] docs: move workflow catalog ownership guidance --- docs/upstream-workflows.md | 80 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/upstream-workflows.md diff --git a/docs/upstream-workflows.md b/docs/upstream-workflows.md new file mode 100644 index 0000000..ac22c42 --- /dev/null +++ b/docs/upstream-workflows.md @@ -0,0 +1,80 @@ + + +# Upstream workflow model + +Upstream files are declared in `upstream/sources.json`. + +Each entry contains: + +- `name`: stable local identifier; +- `repository`, `ref` and `path`: optional tracking metadata used only to discover newer upstream revisions; +- `url`: raw file URL pinned to an immutable upstream commit; +- `sha256`: expected SHA-256 of the downloaded bytes; +- `destination`: repository-relative vendored destination. + +The tracking ref can be mutable. The effective source cannot: after refresh, the +manifest is rewritten to a full commit SHA and content hash before the vendored +file is accepted. + +Rendered downstream templates are declared separately in +`upstream/templates.json`. Each template points to one vendored source, an +ordered patch list and a generated workflow under GitHub's native `workflow-templates/` directory. Template metadata (`*.properties.json`) is maintained locally so LibreCode can provide its own names, descriptions, categories and icons without inheriting upstream branding. + +## Commands + +Synchronize declared immutable sources: + +```bash +python3 scripts/sync_upstream.py sync upstream/sources.json +``` + +Verify committed vendored files without modifying them: + +```bash +python3 scripts/sync_upstream.py check upstream/sources.json +``` + +Resolve tracked refs to their latest commit, recompute SHA-256 and update the +vendored files: + +```bash +python3 scripts/sync_upstream.py refresh upstream/sources.json +``` + +Render vendored workflows with downstream patches: + +```bash +python3 scripts/render_upstream.py sync upstream/templates.json +``` + +The renderer processes every declared template. Successful templates are updated. +If one or more patches no longer apply, those templates are left unchanged and +the renderer returns a structured report containing every failure. + +## Automated refresh + +The scheduled `refresh-upstream.yml` workflow: + +1. resolves each tracked upstream workflow to its latest commit; +2. updates the immutable URL, SHA-256 and vendored bytes; +3. verifies the vendored sources; +4. attempts every downstream patch; +5. runs the test suite; +6. opens one pull request containing the upstream and successfully rendered changes. + +If all patches apply, the pull request is normal. If any patch fails, the pull +request is opened as draft and its body lists each failed template, patch path and +error. The generated template for a failed patch remains at its previous known-good +version. The workflow then fails after creating the pull request so the problem is +also visible in Actions. + +Failures while resolving, downloading or verifying upstream sources are treated +as fatal and do not create a partial update pull request. + +A dedicated `WORKFLOW_UPDATE_TOKEN` secret is required for pull-request creation. +Using only the workflow's `GITHUB_TOKEN` would prevent the resulting pull request +from triggering the normal CI workflows. The refresh itself uses the read-only +`GITHUB_TOKEN` to resolve public upstream commits. From f1f1b166948a7ae193068970c010e70e003dee2a Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:54:43 -0300 Subject: [PATCH 45/49] docs: move workflow catalog ownership guidance --- docs/dependency-update-policy.md | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/dependency-update-policy.md diff --git a/docs/dependency-update-policy.md b/docs/dependency-update-policy.md new file mode 100644 index 0000000..3b54a20 --- /dev/null +++ b/docs/dependency-update-policy.md @@ -0,0 +1,67 @@ + + +# Dependency update and auto-merge policy + +Dependency automation is more privileged than lint or test workflows because it can +create, approve or merge pull requests. It is therefore not automatically copied from +Nextcloud into the LibreCode catalog. + +## Allowed actors + +Automatic approval or merge may only act on pull requests created by explicitly +recognized dependency bots: + +- GitHub Dependabot; +- other bots only after an explicit organization-level decision and equivalent actor + verification. + +Do not auto-approve arbitrary pull requests based only on branch naming or labels. + +## Merge policy + +Default policy: + +- patch and minor dependency updates may be eligible for auto-merge after all required + checks pass; +- major updates require human review unless a repository documents a narrower exception; +- security remediation may create a pull request automatically but must not bypass + required checks; +- approval and merge are separate operations and should remain independently auditable. + +## Credentials + +Prefer the `LibreCode Workflow Automation` GitHub App for cross-repository or +workflow-file mutations. + +Do not require a maintainer's personal access token in a shared template. A PAT-based +workflow stays repository-local until it can be replaced with an organization-owned +credential model. + +## pull_request_target + +A workflow using `pull_request_target` must: + +- verify the pull request actor before any privileged action; +- avoid checking out or executing untrusted pull-request code with write credentials; +- grant only the permissions required for metadata, approval or merge operations; +- pin every external Action to a full commit SHA. + +## Distribution decision + +Dependency/update workflows are cataloged only when their credential and actor model is +generic across LibreCode consumers. + +Repository-specific combinations of Dependabot, Renovate, labels, branch naming or PATs +remain local workflows. The organization catalog should not centralize them merely to +reduce YAML duplication. + +## Current decisions + +- `dependabot-approve-merge.yml`: do not migrate the existing Extract workflow until it + uses the organization policy above and an organization-owned credential model. +- `npm-audit-fix.yml`: keep repository-local; it may create a remediation PR but should + not imply automatic approval/merge. +- obsolete Nextcloud OCP auto-merge workflows are not migrated. From 0ff61aff603176beb064cf2114b33f8a0713c728 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:57:09 -0300 Subject: [PATCH 46/49] ci: avoid unrelated repository-wide REUSE adoption --- .github/workflows/reuse.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/reuse.yml diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml deleted file mode 100644 index eaf21f1..0000000 --- a/.github/workflows/reuse.yml +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: REUSE Compliance Check - -on: - workflow_call: - pull_request: - push: - branches: - - main - -permissions: - contents: read - -jobs: - reuse: - name: REUSE Compliance Check - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Run REUSE - uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 From 16cb6dbe4dd850d4960fa4fa30a6f3db3ff6e720 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:57:17 -0300 Subject: [PATCH 47/49] chore: preserve licenses for migrated catalog --- LICENSES/AGPL-3.0-or-later.txt | 661 +++++++++++++++++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 LICENSES/AGPL-3.0-or-later.txt diff --git a/LICENSES/AGPL-3.0-or-later.txt b/LICENSES/AGPL-3.0-or-later.txt new file mode 100644 index 0000000..fbdf310 --- /dev/null +++ b/LICENSES/AGPL-3.0-or-later.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + libresign + Copyright (C) 2020 LibreCode Coop + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. From d8b80b1f691466eab807e7b3b412f642e8acaa0f Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:57:22 -0300 Subject: [PATCH 48/49] chore: preserve licenses for migrated catalog --- LICENSES/MIT.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 LICENSES/MIT.txt diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..7762a5f --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,15 @@ +Copyright + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and associated documentation files (the “Software”), +to deal in the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From fd621d38b9993b5c5ca556cfbf05cf573251e0d9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 23 Sep 2026 23:57:25 -0300 Subject: [PATCH 49/49] chore: preserve licenses for migrated catalog --- LICENSES/CC0-1.0.txt | 121 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 LICENSES/CC0-1.0.txt diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work.