Skip to content

Instantly share code, notes, and snippets.

@astanziola
Created February 16, 2025 23:49
Show Gist options
  • Save astanziola/02251242ba3d9d5531ceb430e61993e3 to your computer and use it in GitHub Desktop.
Save astanziola/02251242ba3d9d5531ceb430e61993e3 to your computer and use it in GitHub Desktop.
YouTrack Recurrent Issue Workflow
/**
* YouTrack Workflow Rule: Automatic Recurrent Issue Creation
*
* This workflow rule automatically creates a new issue when a recurring issue is marked as Done.
* The new issue inherits properties from the completed issue and is scheduled based on the
* recurrence pattern (Daily, Weekly, Monthly, Quarterly, or Yearly).
*
* Behavior:
* - Triggers when an issue with a RecurrencePattern is marked as Done
* - Creates a new issue with the same summary, description, and recurrence pattern
* - Sets the due date based on either:
* a) The original due date if completed early
* b) The completion date if completed late
* - New issue is created in Open state
*
* Required Fields for Issues:
* - RecurrencePattern (enum): Daily, Weekly, Monthly, Quarterly, Yearly
* - State (enum): Done, Open
* - Due Date (date)
*
* Debug Mode:
* - Set debugLog = true to enable detailed logging
* - Logs include guard checks, date calculations, and issue creation steps
*/
const entities = require('@jetbrains/youtrack-scripting-api/entities');
// Debug flag - set to true to enable debug logging
const debugLog = false;
// Debug logging helper
const debug = (...args) => {
if (debugLog) {
console.log(...args);
}
};
exports.rule = entities.Issue.onChange({
title: 'Create next recurrent issue',
guard: (ctx) => {
debug("=== GUARD FUNCTION STARTED ===");
const issue = ctx.issue;
// Safe logging of issue details
const safeIssueLog = {
id: issue?.id,
summary: issue?.summary,
state: issue?.State?.name,
dueDate: issue?.['Due Date'],
hasRecurrencePattern: !!issue?.RecurrencePattern,
recurrencePattern: issue?.RecurrencePattern?.name
};
debug("Issue details:", safeIssueLog);
if (issue?.State?.name === 'Done' && issue?.RecurrencePattern?.name) {
debug("Guard passing: issue is Done and has recurrence pattern");
return true;
}
debug("Guard returning false: conditions not met");
return false;
},
action: (ctx) => {
debug("=== ACTION FUNCTION STARTED ===");
const issue = ctx.issue;
const recurrencePattern = issue.RecurrencePattern;
const completionDate = new Date();
const originalDueDate = new Date(issue['Due Date']);
debug("Dates:", {
completionDate: completionDate.toISOString(),
originalDueDate: originalDueDate.toISOString(),
isCompletedEarly: completionDate <= originalDueDate
});
const baseDate = completionDate <= originalDueDate ? originalDueDate : completionDate;
debug("Selected base date for calculation:", baseDate.toISOString());
let newDueDate = calculateNextDueDate(baseDate, recurrencePattern);
debug("Calculated new due date:", newDueDate.toISOString());
debug("Creating new issue...");
const newIssue = new entities.Issue(ctx.currentUser, issue.project, issue.summary);
debug("Copying fields to new issue...");
newIssue.description = issue.description;
newIssue.fields.State = ctx.State.Open;
newIssue.fields.RecurrencePattern = ctx.RecurrencePattern[recurrencePattern.name];
newIssue.fields['Due Date'] = newDueDate.getTime(); // Convert to milliseconds timestamp
debug("New issue created with fields:", {
summary: newIssue.summary,
dueDate: newDueDate.toISOString(),
recurrencePattern: recurrencePattern.name,
state: 'Open'
});
},
requirements: {
RecurrencePattern: {
type: entities.EnumField.fieldType,
Daily: {},
Weekly: {},
Monthly: {},
Quarterly: {},
Yearly: {}
},
State: {
type: entities.State.fieldType,
Done: {},
Open: {}
},
'Due Date': {
type: entities.Field.dateType
}
}
});
function calculateNextDueDate(baseDate, pattern) {
debug("=== CALCULATE NEXT DUE DATE STARTED ===");
debug("Inputs:", {
baseDate: baseDate.toISOString(),
pattern: pattern?.name || 'unknown'
});
const date = new Date(baseDate);
switch(pattern.name.toLowerCase()) {
case 'daily':
date.setDate(date.getDate() + 1);
break;
case 'weekly':
date.setDate(date.getDate() + 7);
break;
case 'monthly':
date.setMonth(date.getMonth() + 1);
break;
case 'quarterly':
date.setMonth(date.getMonth() + 3);
break;
case 'yearly':
date.setFullYear(date.getFullYear() + 1);
break;
default:
debug("ERROR: Unknown pattern:", pattern?.name);
throw new Error(`Unknown recurrence pattern: ${pattern?.name}`);
}
debug("Calculated new date:", date.toISOString());
return date;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment