Skip to content

refactor(openapi): resource-oriented paths for /openapi/v1 + difyctl version gate#38367

Merged
wylswz merged 10 commits into
mainfrom
refactor/openapi-resource-oriented-paths
Jul 8, 2026
Merged

refactor(openapi): resource-oriented paths for /openapi/v1 + difyctl version gate#38367
wylswz merged 10 commits into
mainfrom
refactor/openapi-resource-oriented-paths

Conversation

@lin-snow

@lin-snow lin-snow commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

⚠️ Breaking Change

Aligns the user-scoped /openapi/v1 surface (consumed by the difyctl CLI) with our API Design Guide: verb path suffixes are replaced by standard resource GETs and AIP-136 :action custom methods.

Before After
GET /apps/{id}/describe GET /apps/{id}
GET /permitted-external-apps/{id}/describe GET /permitted-external-apps/{id}
POST /apps/{id}/run POST /apps/{id}:run
POST /apps/{id}/tasks/{tid}/stop POST /apps/{id}/tasks/{tid}:stop
POST /apps/{id}/files/upload POST /apps/{id}/files
GET /apps/{id}/export GET /apps/{id}/dsl
GET /apps/{id}/check-dependencies GET /apps/{id}/dependencies:check
POST /workspaces/{ws}/apps/imports/{iid}/confirm POST /workspaces/{ws}/apps/imports/{iid}:confirm
POST /workspaces/{ws}/switch POST /workspaces/{ws}:switch
GET|POST /apps/{id}/form/human_input/{tok} /apps/{id}/human-input-forms/{tok} (+ :submit for POST)
PUT /workspaces/{ws}/members/{mid}/role PATCH /workspaces/{ws}/members/{mid}

difyctl is the only consumer and ships in lockstep with the server, so this is a hard cutover — no /v2, no dual-mount. RFC 8628 device-flow routes and the limit pagination param are intentionally left unchanged.

The change is self-contained in this repo: server routes, the regenerated committed oRPC/Zod contract (packages/contracts) plus a codegen tweak that nests :action methods cleanly (apps.byAppId.run rather than apps.appIdRun), the regenerated Markdown docs, and the difyctl (cli/) call-sites + mock server.

🔀 Version compatibility gate

Because the path cutover is breaking, an outdated difyctl that still calls the old paths would otherwise get a bare 404/405. This PR adds a symmetric version gate so each side refuses a too-old counterpart with a clear "upgrade" message — each side stores the oldest counterpart it supports and hard-fails only on the lower bound (so there are no false positives; "too new" stays a soft nudge):

Side Holds Rejects How
server (/openapi/v1) MIN_DIFYCTL_VERSION = 0.2.0-alpha difyctl < 0.2.0-alpha HTTP 426 Upgrade Required + docs link, via an app-level before_app_request hook so it fires even for removed paths (which no longer match a route and would otherwise 404). Allowlists _version/_health; fails open for non-difyctl / unparseable User-Agents.
difyctl difyctl.compat.minDify = 1.16.0 Dify server < 1.16.0 hard error (exit 6) before running the command, cached per host for 1h (positive-only, so a just-upgraded server clears at once); fails open on probe error; checked fresh at login before persisting the session.

A server 426 also maps to the CLI's version-compat exit code (6). difyctl is bumped to 0.2.0-alpha — the first build that speaks the new paths — and the two floors move together at the cutover (dify 1.16.0 ↔ difyctl 0.2.0-alpha).

Deploy note: the server gate must ship in lockstep with a released difyctl 0.2.0-alpha, so outdated clients have an upgrade target. Deploying the path cutover already implies releasing a new difyctl.

Verification: backend openapi unit tests (449, incl. the new version-gate suite), swagger snapshot tests (37), and the CLI suite (1252) all pass, together with the contract and CLI type-checks, eslint, and the release-manifest validator. Integration/container tests are CI-only.

Screenshots

N/A — backend/CLI API refactor, no UI change.

Checklist

  • This change requires a documentation update: N/A — /openapi/v1 is not part of the public docs; the in-repo generated Markdown is regenerated in this PR.
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly (regenerated api/openapi/markdown/openapi-openapi.md).
  • I ran the relevant lint & type-checks (ruff via pre-commit, backend openapi tests, tsgo for the contract and CLI).

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. refactor labels Jul 3, 2026
@github-actions github-actions Bot added the web This relates to changes on the web. label Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Diff

base → PR
--- /tmp/pyrefly_base.txt	2026-07-07 10:41:19.805917551 +0000
+++ /tmp/pyrefly_pr.txt	2026-07-07 10:41:11.060866487 +0000
@@ -2273,23 +2273,21 @@
 ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]
   --> tests/unit_tests/controllers/openapi/test_workspaces.py:50:12
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:179:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:178:12
 ERROR `in` is not supported between `Literal['POST']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:180:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:179:12
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:185:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:184:12
 ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:186:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:185:12
 ERROR `in` is not supported between `Literal['POST']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:187:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:186:12
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:192:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:191:12
 ERROR `in` is not supported between `Literal['DELETE']` and `None` [not-iterable]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:192:12
+ERROR `in` is not supported between `Literal['PATCH']` and `None` [not-iterable]
    --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:193:12
-ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:198:12
-ERROR `in` is not supported between `Literal['PUT']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:199:12
 ERROR Cannot index into `Iterable[bytes]` [bad-index]
    --> tests/unit_tests/controllers/service_api/app/test_audio.py:186:16
 ERROR Cannot index into `Response` [bad-index]

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 52.40% 52.38% -0.02%
Strict coverage 51.91% 51.89% -0.02%
Typed symbols 32,182 32,184 +2
Untyped symbols 29,514 29,534 +20
Modules 2976 2978 +2

@lin-snow lin-snow self-assigned this Jul 3, 2026
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.57143% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.39%. Comparing base (2c6ec1a) to head (847ae4a).

Files with missing lines Patch % Lines
api/controllers/openapi/_version_gate.py 93.75% 2 Missing ⚠️
cli/src/commands/auth/login/index.ts 0.00% 2 Missing ⚠️
cli/src/version/enforce.ts 89.47% 2 Missing ⚠️
cli/src/cache/compat-store.ts 97.14% 1 Missing ⚠️
cli/src/commands/_shared/authed-command.ts 0.00% 1 Missing ⚠️
cli/src/commands/run/app/run.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #38367      +/-   ##
==========================================
+ Coverage   85.33%   85.39%   +0.05%     
==========================================
  Files        5005     5163     +158     
  Lines      265421   269857    +4436     
  Branches    50474    51554    +1080     
==========================================
+ Hits       226504   230434    +3930     
- Misses      34469    34974     +505     
- Partials     4448     4449       +1     
Flag Coverage Δ
api 85.60% <96.22%> (+<0.01%) ⬆️
cli 88.48% <91.95%> (?)
dify-ui 94.72% <ø> (ø)
web 84.87% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@lin-snow lin-snow force-pushed the refactor/openapi-resource-oriented-paths branch from d883dc1 to cd5a5cb Compare July 3, 2026 05:51
@lin-snow lin-snow requested review from wylswz and removed request for QuantumGhost, crazywoola and laipz8200 July 3, 2026 05:52
@lin-snow lin-snow marked this pull request as draft July 3, 2026 05:52
@lin-snow lin-snow marked this pull request as ready for review July 3, 2026 06:07
Comment thread api/controllers/openapi/app_dsl.py Outdated
@lin-snow lin-snow changed the title refactor(openapi): resource-oriented paths for the /openapi/v1 surface refactor(openapi): resource-oriented paths for /openapi/v1 Jul 3, 2026
GareArc
GareArc previously approved these changes Jul 3, 2026

@GareArc GareArc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 3, 2026
@lin-snow lin-snow added this pull request to the merge queue Jul 3, 2026
@lin-snow lin-snow removed this pull request from the merge queue due to a manual request Jul 3, 2026
@lin-snow lin-snow force-pushed the refactor/openapi-resource-oriented-paths branch 2 times, most recently from d2340d5 to 95a28dd Compare July 3, 2026 11:22
@lin-snow lin-snow changed the title refactor(openapi): resource-oriented paths for /openapi/v1 refactor(openapi): resource-oriented paths for /openapi/v1 + difyctl version gate Jul 3, 2026
@lin-snow lin-snow force-pushed the refactor/openapi-resource-oriented-paths branch from fdd65fb to 1ceee98 Compare July 7, 2026 04:07
@lin-snow lin-snow requested a review from GareArc July 7, 2026 06:34
Comment thread api/controllers/openapi/_version_gate.py
Comment thread api/controllers/openapi/_version_gate.py Outdated
@lin-snow lin-snow requested a review from a team July 7, 2026 08:45
@lin-snow lin-snow force-pushed the refactor/openapi-resource-oriented-paths branch from f18035d to 94345d1 Compare July 7, 2026 08:46
Comment thread api/pyproject.toml Outdated
lin-snow and others added 10 commits July 7, 2026 18:39
…hods

Align the /openapi/v1 surface with the API Design Guide: replace verb path
suffixes with standard resource GETs and AIP-136 `:action` custom methods.

  GET  /apps/{id}/describe                          -> GET  /apps/{id}
  GET  /permitted-external-apps/{id}/describe        -> GET  /permitted-external-apps/{id}
  POST /apps/{id}/run                                -> POST /apps/{id}:run
  POST /apps/{id}/tasks/{tid}/stop                   -> POST /apps/{id}/tasks/{tid}:stop
  POST /apps/{id}/files/upload                       -> POST /apps/{id}/files
  GET  /apps/{id}/export                             -> GET  /apps/{id}/dsl
  GET  /apps/{id}/check-dependencies                 -> GET  /apps/{id}/dependencies
  POST /workspaces/{ws}/apps/imports/{iid}/confirm   -> POST /workspaces/{ws}/apps/imports/{iid}:confirm
  POST /workspaces/{ws}/switch                       -> POST /workspaces/{ws}:switch
  GET|POST /apps/{id}/form/human_input/{tok}         -> /apps/{id}/human-input-forms/{tok} (+ :submit for POST)
  PUT  /workspaces/{ws}/members/{mid}/role           -> PATCH /workspaces/{ws}/members/{mid}

difyctl is the sole consumer and ships in lockstep, so this is a hard cutover
(no /v2, no dual-mount). RFC 8628 device-flow routes and the `limit` pagination
param are intentionally left unchanged.
Teach the oRPC codegen to split AIP-136 `resource:action` segments so custom
methods nest as clean sibling nodes (apps.byAppId.run) instead of collapsing
the param into a fake static key (apps.appIdRun). Regenerate the committed
oRPC/Zod contract and the split Markdown docs from the new spec.

Update the difyctl call-sites whose wire path or generated method name moved
(describe->get, export->dsl, check-dependencies->dependencies, files/upload->
files, human_input->human-input-forms:submit, role->PATCH) and the Hono mock
server + URL assertions in the CLI test suite.
Per review: GET /apps/{id}/dependencies over-promised — the handler returns
only leaked deps (referenced in the DSL but not installed), not the full set.
Model it as the AIP-136 read-only custom method:

  GET /apps/{id}/dependencies  ->  GET /apps/{id}/dependencies:check

This leaves the plain noun free for a future full listing. Regenerate the
contract + Markdown docs and update the difyctl call-site, mock, and tests.
Add an app-level version gate on /openapi/v1 that reads the difyctl version
from the User-Agent and returns 426 Upgrade Required (with an upgrade hint)
when the client is older than MIN_DIFYCTL_VERSION (0.2.0).

Registered via before_app_request so it also fires for requests to removed
paths — those no longer match a route and would otherwise 404 before a
blueprint-scoped hook runs. Allowlists _version/_health and fails open for
non-difyctl or unparseable User-Agents.
Symmetric client-side gate for the difyctl -> dify direction: refuse a Dify
server older than difyctl requires (compat.minDify 1.16.0) with a hard error
(exit 6). Cached per host for 1h, positive-only so a just-upgraded server
clears a previous block immediately; fails open on probe error. Checked
fresh at login before persisting the session, and on every authed command.

Split evaluateCompat's "unsupported" verdict into too_old (hard fail via
enforceDifyVersion) and too_new (kept as the existing soft nudge). Map server
426 responses to the version-compat exit code. Bump difyctl to 0.2.0.
The release manifest validator (a CI-only step) requires the alpha channel's
version to match X.Y.Z-alpha; plain 0.2.0 failed it. Use 0.2.0-alpha and pin
the server floor MIN_DIFYCTL_VERSION to 0.2.0-alpha (0.1.0-alpha still
rejected; the new client accepted).
…pyproject

Address review feedback on the difyctl version gate:

- Compare only the numeric core (major.minor.patch), dropping pre-release/
  channel suffixes from ordering, on both the server gate and the difyctl
  compat check. Prevents a future floor bump (e.g. 0.3.0) from spuriously
  rejecting a 0.3.0-rc.1 build, and simplifies compat.ts.
- Move MIN_DIFYCTL_VERSION out of _version_gate.py into pyproject.toml
  [tool.dify] min_difyctl_version, read via dify_config, so the difyctl
  floor sits next to the server version for release-time paired bumps.
The [tool.dify] comment and _version_gate.py docstring pointed at
dify-todo/inprogress/difyctl/openapi/version.md, a path that only exists in
a private umbrella workspace — a dangling reference in the public repo
(per GareArc review). The 'bump in lockstep' guidance is self-contained
without it.
@lin-snow lin-snow force-pushed the refactor/openapi-resource-oriented-paths branch from 94345d1 to 847ae4a Compare July 7, 2026 10:39

@GareArc GareArc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@wylswz wylswz added this pull request to the merge queue Jul 8, 2026
Merged via the queue into main with commit 9156556 Jul 8, 2026
49 checks passed
@wylswz wylswz deleted the refactor/openapi-resource-oriented-paths branch July 8, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer refactor size:L This PR changes 100-499 lines, ignoring generated files. web This relates to changes on the web.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants