You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

YouTrack命令工作流异常:未显示、误触发及重复通知问题咨询

YouTrack工作流问题排查

问题描述

我有多年编码经验,但刚接触YouTrack工具。写了两个动作工作流:一个是名为RJH_Test的Hello World示例,另一个是给工单标题加前缀的实用工作流。现在遇到三个问题:

  1. 实用工作流没出现在「Apply Command」对话框里,原因是什么?
  2. 调用RJH_Test工作流时,为什么会同时触发实用工作流?
  3. 实用工作流里的guard通知为什么会运行两次?

相关代码

const entities = require('@jetbrains/youtrack-scripting-api/entities');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');

exports.rule = entities.Issue.action({
  title: "Action 2",
  command: 'RJH_Test',
  action: function(ctx) {
    const logger = new Logger(ctx.traceEnabled);

    // --- #1 notifyUser ---
    const message_0 = `Hello Earthling #2`;
    logger.log("Show alert with message", message_0);

    const notifyUserFn_0 = () => {
      workflow.message(message_0);
    };

//    notifyUserFn_0();

  }
});

/**
 * This workflow rule changes the subject of an ticket.
 * The trigger is when the command text is invoked.
 * The subject is prefixed with the following inside square brackets:
 * The code character of the priority followed by one space
 * the customer name, maximum 25 characters, no hyphens, whole words, followed by only one space
 * the due date in ISO format
 *
 * Predefined field 'summary', must be lower case, is the title of the issue
 * Custom field "Priority (SUP)" is type enum (single) with 4 values configured.  These are in the form of text followed by a single capital letter inside normal brackets "()"
 * Custom field "Customer" is type enum (single) with 400 values currently configured.  These are often lengthy and contain hyphens
 * Custom field "Due Date" is type date, to be reformated as an ISO date.
 **/

/* Load global manifest constants */
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
function getWorkflowContext(ctx) {
    return {
        logger: new Logger(ctx.traceEnabled),
        ticketThis: ctx.issue,
        customerField: ctx.CustomerEnum,
        dueDateField: ctx.DueDateField
    };
}

exports.rule = entities.Issue.action({
  title: "Custom Action to prefix the Subject when the command is invoked",
  description: "Prefixes the Subject only when the command is invoked.",
  command: 'ticket-prefix',

  /* The GUARD section checks for the fields needed by ACTION and that the subject has not been updated already */
  guard: function(ctx) {
    workflow.message("Starting Guard");
    const { logger, ticketThis, customerField, dueDateField } = getWorkflowContext(ctx);
    const priorityValue = ctx.issue.fields["Priority (SUP)"]; // Get the selected value
    
    if (!priorityValue) {
        workflow.message("Error: Priority field has no selected value.");
    } else {
//      workflow.message(`Priority Selected Value: ${priorityValue.name}`);
    }
    
    const priorityCodeCharacterMatch = priorityValue.name.match(/\((.)\)/);
//    workflow.message("CodeCharacterMatch:" + priorityCodeCharacterMatch);
//    logger.log("CodeCharacterMatch:" + priorityCodeCharacterMatch);
//    console.log("CodeCharacterMatch:" + priorityCodeCharacterMatch);
    
    const priorityCodeCharacter = priorityCodeCharacterMatch ? priorityCodeCharacterMatch[1] : null;
    if (!priorityCodeCharacter) {
      workflow.message("Unable to extract the code character from Priority (SUP).");
      logger.error("Unable to extract the code character from Priority (SUP).");
      return false;
    }else{
//      workflow.message(`Extracted Priority Code: ${priorityCodeCharacter}`);
    }
    
    if (!customerField) {
      workflow.message("Customer field has no value.");
      logger.error("Customer field has no value.");
      return false;
    }

    if (!dueDateField) {
      workflow.message("Due Date field has no value.");
      logger.error("Due Date field has no value.");
      return false;
    }
    
    const existingSummary = ticketThis.summary;
//    workflow.message("Summary= " + existingSummary);
    if (existingSummary.startsWith("[")) {
      workflow.message("Subject already has a prefix. Skipping.");
      logger.warn("Subject already has a prefix. Skipping.");
      return false;
    }

    workflow.message("Ending Guard");
    return true;  // Ensure the guard explicitly returns a boolean
  },
  
  /* ACTION: this can only run if GUARD has returned logical TRUE, and not thrown an error. */
  action: function(ctx) {
    const { logger, ticketThis, customerField, dueDateField } = getWorkflowContext(ctx);
    logger.log("Mode: set");
    logger.log("Current Subject:", ticketThis.fields.Summary);
    workflow.message("Starting Action");
    var existingSummary = ticketThis.summary;
    logger.log("Summary= " + existingSummary);
    console.log("Summary= " + existingSummary);
    workflow.message("Summary= " + existingSummary);
    
    const priorityValue = ctx.issue.fields["Priority (SUP)"]; // Get the selected value
    const priorityCodeCharacterMatch = priorityValue.name.match(/\((.)\)/); // if missing handled in the guard section
    const priorityCodeCharacter = priorityCodeCharacterMatch ? priorityCodeCharacterMatch[1] : null;

    let processedCustomerName = customerField.name
      .replace(/-/g, ' ')   // Replace hyphens with spaces
      .replace(/\s+/g, ' ') // Replace multiple spaces with a single space
      .trim();              // Remove leading and trailing spaces
    if (processedCustomerName.length > 25) {
      const truncated = processedCustomerName.slice(0, 25);
      const lastSpaceIndex = truncated.lastIndexOf(' ');
      processedCustomerName = lastSpaceIndex > 0 ? truncated.slice(0, lastSpaceIndex) : truncated;
    }
    logger.log(`Processed customer name: "${processedCustomerName}"`);
    
    const isoDueDate = new Date(dueDateField).toISOString().split('T')[0];   // Convert the date to ISO format
    logger.log(`Formatted Due Date (ISO): ${isoDueDate}`);

// Build new prefix
    const prefix = `[${priorityCodeCharacter} ${isoDueDate}][${processedCustomerName}]`;
    logger.log(`Prefix: ${prefix}`);
    
    ticketThis.summary = prefix + " " + ticketThis.summary;
    logger.log("Replaced Subject:", ticketThis.fields.summary);
  },
  
  /* REQUIREMENTS: this section ensures that the field is defined and of the correct type.  The GUARD section however needs to check the field contents are useful.*/
  requirements: {
    PrioritySUPEnum: {
      name: "Priority (SUP)",
      type: entities.EnumField.fieldType
    },
    CustomerEnum: {
      name: "Customer",
      type: entities.EnumField.fieldType
    },
    DueDateField: {
      name: "Due Date",
      type: entities.Field.dateType
    }
  }
});

问题解答

1. 实用工作流未出现在Apply Command对话框的原因

  • 项目与权限配置缺失:检查工作流是否已关联到目标项目,以及当前用户是否拥有执行该工作流的权限(在YouTrack的「Workflow Administration」中确认项目绑定和权限设置)。
  • 工作流未发布:未发布的工作流不会显示在命令列表中,需确保实用工作流已完成发布操作。
  • Requirements字段校验失败:工作流的requirements部分定义了三个必填字段,如果当前项目未配置Priority (SUP)、Customer或Due Date,工作流会被禁用。可在工作流的「Validation」标签页查看字段缺失的错误提示。

2. 调用RJH_Test时触发实用工作流的原因

  • 脚本文件合并问题:两个工作流写在同一个脚本文件中,YouTrack会将它们视为同一规则集合,执行时会同时触发。需将两个工作流拆分为独立的脚本文件,分别发布。
  • 触发规则配置错误:若实用工作流的触发类型被误设为「工单更新」而非仅绑定ticket-prefix命令,会在RJH_Test执行(工单发生变更)时被触发。确认实用工作流的触发类型为action且仅绑定指定命令。

3. Guard通知运行两次的原因

  • YouTrack预检查机制:YouTrack在显示命令选项前,会预执行一次guard判断工作流是否可用;用户实际执行命令时,会再次执行guard做最终校验,因此会出现两次通知输出。
  • 优化方案:通过ctx.isCommandExecution区分预检查和实际执行,仅在实际执行时显示通知:
    guard: function(ctx) {
      // 仅在实际执行命令时输出通知
      if (ctx.isCommandExecution) {
        workflow.message("Starting Guard");
      }
      // 其余逻辑保持不变
    }
    

内容的提问来源于stack exchange,提问作者Robert Howe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 02:54:53