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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## Description

<!-- What do we want to achieve with this PR? -->

## Relevant Technical Choices

<!-- For Code Reviewers: Please describe your changes. -->

## Testing Instructions

<!-- For someone doing QA: How can the changes in this PR be tested? Please provide step-by-step instructions to test the changes. -->

## Additional Information:

<!-- Include any other context, links, or references that reviewers or QA should be aware of. -->

## Screenshot/Screencast

<!-- Add visual aids to demonstrate the changes made in this PR, if applicable. -->


## Checklist

<!-- Check these after creating PR, use NA if something is not applicable -->

- [ ] I have carefully reviewed the code before submitting it for review.
- [ ] This code is adequately covered by unit tests to validate its functionality.
- [ ] I have conducted thorough testing to ensure it functions as intended.
- [ ] A member of the QA team has reviewed and tested this PR (To be checked by QA or code reviewer)

<!--
Example:

Fixes #123
Partially addresses #22
See #834
-->

Fixes #
58 changes: 58 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Bench Build Test

on:
pull_request:

concurrency:
group: ${{ github.repository }}-${{ github.event.number }}
cancel-in-progress: true

jobs:
Bench-Build-Test:
runs-on: ubuntu-latest
container:
image: docker.io/frappe/bench:latest
options: --user root

steps:
- name: Setup Github ENV
run: |
echo "HOME=/home/frappe" >> $GITHUB_ENV
echo "PATH=/home/frappe/.local/bin:$PATH" >> $GITHUB_ENV

- name: Create a new minimal bench
run: |
cd /home/frappe
su frappe bash -c "bench init frappe-bench --skip-redis-config-generation --no-procfile --skip-assets --frappe-branch version-16"

- name: Get Dependent Apps
run: |
cd /home/frappe/frappe-bench
# Use public URL for public repos, authenticated URL for private repos
if [ "${{ github.event.pull_request.head.repo.private }}" = "false" ]; then
# This is a public repository (could be a fork or not)
git clone https://github.com/${{ github.event.pull_request.head.repo.full_name }} -b ${{ github.event.pull_request.head.ref }} --depth=1 app_repo
else
# Private repository, use authentication
git clone https://rtbot:${{ secrets.RTBOT_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }} -b ${{ github.event.pull_request.head.ref }} --depth=1 app_repo
fi
DEPS=$(grep -E "required_apps\s*=\s*\[" app_repo/*/hooks.py | sed 's/.*\[\(.*\)\]/\1/g' | tr -d '"' | tr -d "'" | tr ',' '\n' | awk '{$1=$1};1')
rm -rf app_repo
for dep in $DEPS; do
su frappe bash -c "bench get-app $dep"
done

- name: Get APP and Build
run: |
cd /home/frappe/frappe-bench
if [ "${{ github.event.pull_request.head.repo.private }}" = "false" ]; then
# Public fork
su frappe bash -c "bench get-app https://github.com/${{ github.event.pull_request.head.repo.full_name }} --branch ${{ github.event.pull_request.head.ref }}"
else
# Private fork (requires token)
su frappe bash -c "bench get-app https://rtbot:${{ secrets.RTBOT_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }} --branch ${{ github.event.pull_request.head.ref }}"
fi

- name: Cleanup
if: ${{ always() }}
uses: rtCamp/action-cleanup@master
Comment thread
HarishChandran3304 marked this conversation as resolved.
52 changes: 52 additions & 0 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Linters

on:
pull_request:
workflow_dispatch:
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
pre-commit:
name: "Frappe Linter"
runs-on: ubuntu-latest
container:
image: alpine:latest # latest used here for simplicity, not recommended
defaults:
run:
shell: sh
steps:
- name: fix tar dependency in alpine container image
run: |
apk --no-cache add tar nodejs npm python3 git bash py3-pip
npm install -g prettier
# check python modules installed versions
python3 -m pip freeze --local
pip install pre-commit --break-system-packages

- uses: actions/checkout@v6
- run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*

- name: Get changed files
id: file_changes
run: |
export DIFF=$(git diff --name-only origin/${{ github.base_ref }} ${{ github.sha }})
echo "Diff between ${{ github.base_ref }} and ${{ github.sha }}"
echo "files=$( echo "$DIFF" | xargs echo )" >> $GITHUB_OUTPUT


- name: Cache pre-commit since we use pre-commit from container
uses: actions/cache@v5
with:
path: ~/.cache/pre-commit
key: pre-commit-3|${{ hashFiles('.pre-commit-config.yaml') }}

- name: Execute pre-commit
run: |
pre-commit run --color=always --show-diff-on-failure --files ${{ steps.file_changes.outputs.files }}
69 changes: 69 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Unit Tests
permissions:
contents: read

on:
push:
branches: [version-16, version-16-hotfix]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
Tests:
runs-on: ubuntu-latest

services:
redis-cache:
image: redis:alpine
ports: ["13000:6379"]
redis-queue:
image: redis:alpine
ports: ["11000:6379"]
mariadb:
image: mariadb:10.6
env:
MYSQL_ROOT_PASSWORD: root
ports: ["3306:3306"]
options: --health-cmd="mysqladmin ping -uroot -proot" --health-interval=5s --health-timeout=2s --health-retries=3

steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v6
with:
python-version: "3.14"

- uses: actions/setup-node@v6
with:
node-version: 24

- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-

- name: Setup bench
run: |
pip install frappe-bench
bench init --skip-redis-config-generation --skip-assets --frappe-branch version-16 --python "$(which python)" ~/frappe-bench
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"

- name: Install
working-directory: /home/runner/frappe-bench
run: |
bench get-app frappe_openapi $GITHUB_WORKSPACE
bench new-site --db-root-password root --admin-password admin --no-mariadb-socket test_site
bench --site test_site install-app frappe_openapi
env:
CI: "Yes"

- name: Run tests
working-directory: /home/runner/frappe-bench
run: |
bench --site test_site set-config allow_tests true
bench --site test_site run-tests --app frappe_openapi
3 changes: 1 addition & 2 deletions .semgrepignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,4 @@ test*.py
.github/

# Markdown files
*.md
frappe_skeleton/__init__.py
*.md
58 changes: 35 additions & 23 deletions frappe_openapi/api/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
# parser picks it up, and each docstring exercises a different aspect of the
# comment format:
#
# 1. demo_health_check minimal GET, guest-accessible, no parameters
# 2. demo_list_items GET with multiple typed query params, pagination
# 3. demo_create_item POST with required + optional body params, rich
# 1. demo_health_check - minimal GET, guest-accessible, no parameters
# 2. demo_list_items - GET with multiple typed query params, pagination
# 3. demo_create_item - POST with required + optional body params, rich
# Returns example that drives the response schema
# 4. demo_update_item PUT showing path-style name in params, optional fields
# 5. demo_delete_item DELETE showing boolean response
# 6. demo_upload_file POST with binary / mixed types
# 7. demo_authenticated endpoint that requires auth (no allow_guest)
# 4. demo_update_item - PUT showing path-style name in params, optional fields
# 5. demo_delete_item - DELETE showing boolean response
# 6. demo_upload_file - POST with binary / mixed types
# 7. demo_authenticated - endpoint that requires auth (no allow_guest)
# ---------------------------------------------------------------------------

from __future__ import annotations
Expand All @@ -28,24 +28,30 @@

def _check_demo_enabled():
"""Check if demo endpoints should be enabled.

Demo endpoints are only enabled in:
1. Developer mode
2. When frappe_openapi_enable_demos site config is True
"""
if frappe.conf.developer_mode:
return True

if frappe.conf.get("frappe_openapi_enable_demos"):
return True

frappe.throw("Demo endpoints are disabled in production. Enable developer mode or set frappe_openapi_enable_demos=1 in site_config.json",
frappe.PermissionError)

frappe.throw(
frappe._(
"Demo endpoints are disabled in production. Enable developer mode or set frappe_openapi_enable_demos=1 in site_config.json"
),
frappe.PermissionError,
)


# ---------------------------------------------------------------------------
# 1. Health check minimal, guest, GET
# 1. Health check - minimal, guest, GET
# ---------------------------------------------------------------------------
# Demo endpoint, intentionally guest-accessible; gated by _check_demo_enabled() at runtime.
# nosemgrep: frappe-semgrep.rules.security.guest-whitelisted-method
@frappe.whitelist(allow_guest=True, methods=["GET"])
def demo_health_check() -> dict[str, Any]:
"""Return a simple liveness probe for the API.
Expand All @@ -68,8 +74,10 @@ def demo_health_check() -> dict[str, Any]:


# ---------------------------------------------------------------------------
# 2. List items GET with typed query params and pagination
# 2. List items - GET with typed query params and pagination
# ---------------------------------------------------------------------------
# Demo endpoint, intentionally guest-accessible; gated by _check_demo_enabled() at runtime.
# nosemgrep: frappe-semgrep.rules.security.guest-whitelisted-method
@frappe.whitelist(allow_guest=True, methods=["GET"])
def demo_list_items(
page: int = 1,
Expand Down Expand Up @@ -112,12 +120,12 @@ def demo_list_items(
}
"""
_check_demo_enabled()
# Demo implementation not executed in production
# Demo implementation - not executed in production
return {"total": 0, "page": page, "page_size": page_size, "items": []}


# ---------------------------------------------------------------------------
# 3. Create item POST with required + optional params, rich response example
# 3. Create item - POST with required + optional params, rich response example
# ---------------------------------------------------------------------------
@frappe.whitelist(methods=["POST"])
def demo_create_item(
Expand Down Expand Up @@ -164,7 +172,7 @@ def demo_create_item(


# ---------------------------------------------------------------------------
# 4. Update item PUT showing optional partial update
# 4. Update item - PUT showing optional partial update
# ---------------------------------------------------------------------------
@frappe.whitelist(methods=["PUT"])
def demo_update_item(
Expand Down Expand Up @@ -201,7 +209,7 @@ def demo_update_item(


# ---------------------------------------------------------------------------
# 5. Delete item DELETE showing boolean response
# 5. Delete item - DELETE showing boolean response
# ---------------------------------------------------------------------------
@frappe.whitelist(methods=["DELETE"])
def demo_delete_item(
Expand Down Expand Up @@ -231,8 +239,10 @@ def demo_delete_item(


# ---------------------------------------------------------------------------
# 6. Upload file POST with binary / mixed types
# 6. Upload file - POST with binary / mixed types
# ---------------------------------------------------------------------------
# Demo endpoint, intentionally guest-accessible; gated by _check_demo_enabled() at runtime.
# nosemgrep: frappe-semgrep.rules.security.guest-whitelisted-method
@frappe.whitelist(allow_guest=True, methods=["POST"])
def demo_upload_file(
file_url: str,
Expand All @@ -259,7 +269,7 @@ def demo_upload_file(
(default: True).
max_width (int, optional): Resize image so its width does not exceed this value
in pixels (aspect ratio preserved). No resizing when omitted.
quality (int, optional): JPEG/WebP encoding quality 1100 (default: 85).
quality (int, optional): JPEG/WebP encoding quality 1-100 (default: 85).
Only applies when ``optimize`` is True.

Returns:
Expand All @@ -277,7 +287,7 @@ def demo_upload_file(


# ---------------------------------------------------------------------------
# 7. Authenticated endpoint requires Bearer token / API key
# 7. Authenticated endpoint - requires Bearer token / API key
# ---------------------------------------------------------------------------
@frappe.whitelist(methods=["GET"])
def demo_authenticated(
Expand Down Expand Up @@ -308,10 +318,12 @@ def demo_authenticated(
}
"""
_check_demo_enabled()
return {
response = {
"success": True,
"resource_id": resource_id,
"owner": frappe.session.user,
"data": {},
"metadata": {} if include_metadata else {},
}
if include_metadata:
response["metadata"] = {}
return response
Loading
Loading