Files
dify/api/controllers/openapi/app_release.py
T
GareArc f22362a892 fix: harden agent login, check more of the DSL, and trim the difyctl skill
Server:
- describe, check and fill human-input nodes with Dify's own model, delivery
  methods included; human-input action ids and __timeout are valid branches
- refuse End nodes that share an output name, as the console's publish does
- warn on a branch with no edge
- the release check counts a run as tested when its graph runs the same as
  the draft, so editor-only fields no longer reset it

CLI:
- login needs --server; there is no default server
- not_logged_in names the config folder it checked
- on Linux without a DBus session, keep the token in tokens.yml, since the
  keyring there is lost when the session ends

Skill:
- log in with --no-wait only, then --resume after the user approves
- report problems instead of substituting, and get a yes before building any
  change from the spec or plan
- trim every file under 500 words
2026-10-09 22:43:37 -07:00

123 lines
4.6 KiB
Python

from __future__ import annotations
from dataclasses import asdict
from http import HTTPStatus
from typing import Final
from flask_restx import Resource
from controllers.openapi import openapi_ns
from controllers.openapi._contract import Example, Kind, endpoint, op_of
from controllers.openapi._models import (
DraftChanges,
Hint,
ReleaseCheckName,
ReleaseCheckResponse,
ReleaseCheckRow,
ReleaseState,
)
from controllers.openapi.app_dsl import AppDslExportApi, issue_row, node_credential_check
from controllers.openapi.app_run import _RUN_ROUTES, require_mode
from controllers.openapi.app_workflow import GRAPH_MODES, VERSION_READ_GUARDS, require_draft
from controllers.openapi.auth.context import Context
from core.app.entities.app_invoke_entities import InvokeFrom
from extensions.ext_application_services import application_services
from graphon.enums import WorkflowExecutionStatus
from models import AppMode, WorkflowRunTriggeredFrom
from models.workflow import Workflow
from services.workflow.graph_check import check_graph
from services.workflow.graph_diff import WorkflowSnapshot, diff_workflows, same_graph
from services.workflow_service import WorkflowService
_DRAFT_TEST_OPS: Final = {
mode: route.op for route in _RUN_ROUTES if route.invoke_from is InvokeFrom.DEBUGGER for mode in route.modes
}
def _snapshot(workflow: Workflow) -> WorkflowSnapshot:
return WorkflowSnapshot(
graph=workflow.graph_dict,
features=workflow.features_dict,
environment_variable_names=frozenset(v.name for v in workflow.environment_variables),
)
@openapi_ns.route("/apps/<string:app_id>/release:check")
class ReleaseCheckApi(Resource):
@endpoint(
op="check.console_app.release",
kind=Kind.OBJECT,
summary="Is the draft ready to publish: draft valid, last test passed on this draft, and what changed",
examples=(Example(title="Before publishing", input={"app_id": "<app_id>"}),),
requirements=VERSION_READ_GUARDS,
returns=(HTTPStatus.OK, ReleaseCheckResponse, "Release check"),
)
def get(self, ctx: Context, app_id: str):
require_mode(ctx.app, *GRAPH_MODES)
draft = require_draft(ctx)
mode = AppMode(ctx.app.mode)
service = WorkflowService()
environment = {v.name: v for v in draft.environment_variables}
issues = check_graph(
draft.graph_dict,
mode=mode,
resources=node_credential_check(ctx.workspace.id, environment, ctx.session),
)
runs = (
application_services()
.workflow_runs.get_paginate_workflow_runs(
ctx.request_context,
app_id=ctx.app.id,
args={"limit": 1},
triggered_from=WorkflowRunTriggeredFrom.DEBUGGING,
)
.data
)
last = runs[0] if runs else None
tested = (
last is not None
and last.status == WorkflowExecutionStatus.SUCCEEDED
and same_graph(last.graph_dict, draft.graph_dict)
)
published = service.get_published_workflow(ctx.app, session=ctx.session)
diff = diff_workflows(_snapshot(published) if published else None, _snapshot(draft))
checks = [
ReleaseCheckRow(
name=ReleaseCheckName.DRAFT_VALID,
passed=not issues,
detail=f"{len(issues)} issue(s)" if issues else "No issues",
),
ReleaseCheckRow(
name=ReleaseCheckName.TESTED,
passed=tested,
detail="The last draft test passed on this draft"
if tested
else "No passing draft test on the current draft",
),
]
hints = []
if issues:
hints.append(
Hint(
summary="Export the draft, fix each issue by its loc, then check and import",
op=op_of(AppDslExportApi.get),
input={"app_id": ctx.app.id},
)
)
if not tested:
hints.append(
Hint(
summary="Run the draft on the acceptance cases",
op=_DRAFT_TEST_OPS[mode],
input={"app_id": ctx.app.id},
)
)
return ReleaseCheckResponse(
ready=all(c.passed for c in checks),
checks=checks,
issues=[issue_row(i) for i in issues],
state=ReleaseState(service_api_enabled=ctx.app.enable_api, webapp_enabled=ctx.app.enable_site),
changes=DraftChanges.model_validate(asdict(diff)),
hints=hints,
)