acdev 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/package.json +1 -1
- package/public/app.js +273 -18
- package/public/index.html +14 -7
- package/public/styles.css +37 -0
- package/src/afterPrRules.js +19 -3
- package/src/config.js +1 -1
- package/src/github.js +439 -1
- package/src/jira.js +178 -0
- package/src/server.js +104 -7
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ The tool starts a local server (default port `4848`), opens the browser, and sho
|
|
|
148
148
|
1. Paste GitHub issue URLs or Jira keys/URLs on **Overview** and click **Add to queue**
|
|
149
149
|
2. Jobs process sequentially: sync base branch → create worktree → run agent → await review
|
|
150
150
|
3. Monitor live logs on **Runs**; review diffs on **Review**
|
|
151
|
-
4. **Approve as draft** (draft PR) or **Approve as ready for review** (non-draft PR)
|
|
151
|
+
4. **Approve as draft** (draft PR) or **Approve as ready for review** (non-draft PR); approval now opens PR and removes local worktree. **Reject** still cleanup worktree, keep history as discarded
|
|
152
152
|
5. **Clear** removes a job from history entirely (so you can re-queue the same issue). Allowed for queued / awaiting review / PR opened / failed / discarded; refused while a job is in-flight
|
|
153
153
|
|
|
154
154
|
### Web UI views
|
|
@@ -160,7 +160,7 @@ The tool starts a local server (default port `4848`), opens the browser, and sho
|
|
|
160
160
|
| **Review** | Edit PR title/body, colored diff, approve draft or ready, reject, or Clear |
|
|
161
161
|
| **Logs** | Cross-job activity log with job filter |
|
|
162
162
|
| **Alerts** | Failures / warnings with Retry, Clear, and dismiss |
|
|
163
|
-
| **Settings** | Ticket source (GitHub / Jira), GitHub + Claude authentication, Jira connection,
|
|
163
|
+
| **Settings** | Ticket source (GitHub / Jira), GitHub + Claude authentication, Jira connection, and `.acdev/config.json` fields |
|
|
164
164
|
|
|
165
165
|
Model selection lives in **Settings → Configuration**. The sidebar shows the current model as a read-only label. Default is **Sonnet 5** (`claude-sonnet-5`).
|
|
166
166
|
|
|
@@ -206,7 +206,7 @@ On first run, creates `.acdev/config.json`:
|
|
|
206
206
|
|
|
207
207
|
| Action | Jira | GitHub |
|
|
208
208
|
|--------|------|--------|
|
|
209
|
-
| `set_status` | Workflow transition to a status whose name matches `targetStatus` (case-insensitive)
|
|
209
|
+
| `set_status` | Workflow transition to a status whose name matches `targetStatus` (case-insensitive). Settings loads live board statuses. | Sets GitHub Projects v2 **Status** (adds the issue to the project if needed); falls back to a label with that name |
|
|
210
210
|
| `add_label` | Adds the Jira label via REST `update.labels` `{ add }` | Adds the issue label via `gh` |
|
|
211
211
|
| `close_issue` | Transitions to a Done-category status, or else a Done/Closed/Resolved-like name | Closes the GitHub issue |
|
|
212
212
|
|
|
@@ -264,6 +264,8 @@ npm test
|
|
|
264
264
|
| `GET` | `/api/models` | `{ models, selected, source }` — live Anthropic list or curated fallback |
|
|
265
265
|
| `PATCH` | `/api/config` | Partial update including `ticketSource`, `jiraBaseUrl`; secrets (`jiraEmail`, etc.) → `.env` |
|
|
266
266
|
| `POST` | `/api/jira/test` | Test Jira credentials (`GET /rest/api/3/myself`) |
|
|
267
|
+
| `GET` | `/api/jira/statuses` | Live Jira board/workflow statuses for Rules |
|
|
268
|
+
| `GET` | `/api/github/statuses` | Live GitHub Project Status options (or labels) |
|
|
267
269
|
|
|
268
270
|
### Branch naming
|
|
269
271
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -125,7 +125,7 @@ let reviewFileLists = {};
|
|
|
125
125
|
/** @type {Record<string, Set<string>>} excluded paths per job while reviewing */
|
|
126
126
|
let reviewExcludedByJob = {};
|
|
127
127
|
let settingsSaving = false;
|
|
128
|
-
/** @type {'ticket' | 'auth' | '
|
|
128
|
+
/** @type {'ticket' | 'auth' | 'config'} */
|
|
129
129
|
let settingsTab = 'ticket';
|
|
130
130
|
/** Sentinel: no model selected (jobs blocked until user picks one). */
|
|
131
131
|
const NO_MODEL = '-';
|
|
@@ -162,6 +162,10 @@ let availableModels = [];
|
|
|
162
162
|
let ticketSource = 'github';
|
|
163
163
|
/** @type {'claude' | 'openrouter'} */
|
|
164
164
|
let llmProvider = 'claude';
|
|
165
|
+
/** @type {Array<{ name: string, column?: string, board?: string }>} */
|
|
166
|
+
let jiraStatusOptions = [];
|
|
167
|
+
/** @type {Array<{ name: string, projectTitle?: string }>} */
|
|
168
|
+
let githubStatusOptions = [];
|
|
165
169
|
/** @type {{
|
|
166
170
|
* repoName?: string,
|
|
167
171
|
* baseBranch?: string,
|
|
@@ -297,12 +301,12 @@ const els = {
|
|
|
297
301
|
settingsModelValue: document.getElementById('settings-model-value'),
|
|
298
302
|
settingsModelPanel: document.getElementById('settings-model-panel'),
|
|
299
303
|
settingsModelList: document.getElementById('settings-model-list'),
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
reviewPrLink: document.getElementById('review-pr-link'),
|
|
305
|
+
reviewPrStatusLine: document.getElementById('review-pr-status-line'),
|
|
306
|
+
reviewPrStatusNotes: document.getElementById('review-pr-status-notes'),
|
|
307
|
+
reviewTerminalSection: document.getElementById('review-terminal-section'),
|
|
308
|
+
reviewTerminalMessage: document.getElementById('review-terminal-message'),
|
|
309
|
+
reviewRetryBtn: document.getElementById('review-retry-btn'),
|
|
306
310
|
settingsTools: document.getElementById('settings-tools'),
|
|
307
311
|
settingsFeedback: document.getElementById('settings-feedback'),
|
|
308
312
|
settingsSaveBtn: document.getElementById('settings-save-btn'),
|
|
@@ -319,12 +323,14 @@ const els = {
|
|
|
319
323
|
settingsJiraRuleAction: document.getElementById('settings-jira-rule-action'),
|
|
320
324
|
settingsJiraRuleStatus: document.getElementById('settings-jira-rule-status'),
|
|
321
325
|
settingsJiraRuleStatusField: document.getElementById('settings-jira-rule-status-field'),
|
|
326
|
+
settingsJiraRuleStatusHint: document.getElementById('settings-jira-rule-status-hint'),
|
|
322
327
|
settingsJiraRuleLabel: document.getElementById('settings-jira-rule-label'),
|
|
323
328
|
settingsJiraRuleLabelField: document.getElementById('settings-jira-rule-label-field'),
|
|
324
329
|
settingsGithubRuleEnabled: document.getElementById('settings-github-rule-enabled'),
|
|
325
330
|
settingsGithubRuleAction: document.getElementById('settings-github-rule-action'),
|
|
326
331
|
settingsGithubRuleStatus: document.getElementById('settings-github-rule-status'),
|
|
327
332
|
settingsGithubRuleStatusField: document.getElementById('settings-github-rule-status-field'),
|
|
333
|
+
settingsGithubRuleStatusHint: document.getElementById('settings-github-rule-status-hint'),
|
|
328
334
|
settingsGithubRuleLabel: document.getElementById('settings-github-rule-label'),
|
|
329
335
|
settingsGithubRuleLabelField: document.getElementById('settings-github-rule-label-field'),
|
|
330
336
|
settingsGhStatus: document.getElementById('settings-gh-status'),
|
|
@@ -935,14 +941,64 @@ function jobRepoLine(job) {
|
|
|
935
941
|
const branch = name ? ` · ${name}` : '';
|
|
936
942
|
return { repo, number: num, branch, text: `${repo} #${num}${branch}`, isJira: false };
|
|
937
943
|
}
|
|
944
|
+
function prDecisionLabel(decision) {
|
|
945
|
+
switch (String(decision || '').trim().toUpperCase()) {
|
|
946
|
+
case 'APPROVED':
|
|
947
|
+
return 'approved';
|
|
948
|
+
case 'CHANGES_REQUESTED':
|
|
949
|
+
return 'changes requested';
|
|
950
|
+
case 'REVIEW_REQUIRED':
|
|
951
|
+
return 'review required';
|
|
952
|
+
default:
|
|
953
|
+
return '';
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function jobPrStatusSummary(job) {
|
|
958
|
+
const pr = job?.prStatus;
|
|
959
|
+
if (!pr) return '';
|
|
960
|
+
if (!pr.ok) {
|
|
961
|
+
return `GitHub status unavailable${pr.error ? ` · ${truncate(pr.error, 72)}` : ''}`;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const parts = [];
|
|
965
|
+
if (pr.state === 'MERGED') {
|
|
966
|
+
parts.push('Merged');
|
|
967
|
+
} else if (pr.state === 'CLOSED') {
|
|
968
|
+
parts.push('Closed');
|
|
969
|
+
} else if (pr.isDraft) {
|
|
970
|
+
parts.push('Draft');
|
|
971
|
+
} else {
|
|
972
|
+
parts.push('Open');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const decision = prDecisionLabel(pr.reviewDecision);
|
|
976
|
+
if (decision) parts.push(decision);
|
|
977
|
+
if (pr.mergeStateStatus) {
|
|
978
|
+
parts.push(`merge ${pr.mergeStateStatus.toLowerCase().replace(/_/g, ' ')}`);
|
|
979
|
+
}
|
|
980
|
+
if (Array.isArray(pr.requestedReviewers) && pr.requestedReviewers.length) {
|
|
981
|
+
parts.push(`requested ${pr.requestedReviewers.join(', ')}`);
|
|
982
|
+
}
|
|
983
|
+
const changeRequest = Array.isArray(pr.latestReviews)
|
|
984
|
+
? pr.latestReviews.find((review) => review.state === 'CHANGES_REQUESTED' && review.body)
|
|
985
|
+
: null;
|
|
986
|
+
if (changeRequest?.body) {
|
|
987
|
+
const who = changeRequest.author ? `${changeRequest.author}: ` : '';
|
|
988
|
+
parts.push(`CR ${who}${truncate(changeRequest.body, 72)}`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return `GitHub · ${parts.join(' · ')}`;
|
|
992
|
+
}
|
|
938
993
|
|
|
939
994
|
function jobSubLine(job) {
|
|
940
995
|
const meta = jobRepoLine(job);
|
|
941
996
|
const agent = jobLlmLine(job);
|
|
997
|
+
const pr = jobPrStatusSummary(job);
|
|
942
998
|
const base = meta.isJira
|
|
943
999
|
? `${meta.number}${meta.branch}`
|
|
944
1000
|
: `${meta.repo} #${meta.number}${meta.branch}`;
|
|
945
|
-
return agent
|
|
1001
|
+
return [base, agent, pr].filter(Boolean).join(' · ');
|
|
946
1002
|
}
|
|
947
1003
|
|
|
948
1004
|
/**
|
|
@@ -995,6 +1051,62 @@ function sortedJobs() {
|
|
|
995
1051
|
);
|
|
996
1052
|
}
|
|
997
1053
|
|
|
1054
|
+
/**
|
|
1055
|
+
* @param {HTMLElement | null} container
|
|
1056
|
+
* @param {object | null | undefined} pr
|
|
1057
|
+
*/
|
|
1058
|
+
function renderPrStatusNotes(container, pr) {
|
|
1059
|
+
if (!container) return;
|
|
1060
|
+
container.innerHTML = '';
|
|
1061
|
+
if (!pr || !pr.ok) return;
|
|
1062
|
+
|
|
1063
|
+
const reviews = Array.isArray(pr.latestReviews) ? [...pr.latestReviews] : [];
|
|
1064
|
+
reviews.sort((a, b) => {
|
|
1065
|
+
const ta = Date.parse(a.submittedAt || '') || 0;
|
|
1066
|
+
const tb = Date.parse(b.submittedAt || '') || 0;
|
|
1067
|
+
return tb - ta;
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
if (reviews.length > 0) {
|
|
1071
|
+
const heading = document.createElement('div');
|
|
1072
|
+
heading.className = 'pr-status-notes-title';
|
|
1073
|
+
heading.textContent = 'Latest reviews';
|
|
1074
|
+
container.appendChild(heading);
|
|
1075
|
+
|
|
1076
|
+
const list = document.createElement('div');
|
|
1077
|
+
list.className = 'pr-status-note-list';
|
|
1078
|
+
|
|
1079
|
+
for (const review of reviews.slice(0, 4)) {
|
|
1080
|
+
const item = document.createElement('div');
|
|
1081
|
+
item.className = 'pr-status-note';
|
|
1082
|
+
const meta = document.createElement('div');
|
|
1083
|
+
meta.className = 'pr-status-note-meta';
|
|
1084
|
+
const parts = [review.state.replace(/_/g, ' ').toLowerCase()];
|
|
1085
|
+
if (review.author) parts.push(review.author);
|
|
1086
|
+
if (review.submittedAt) parts.push(formatTime(review.submittedAt));
|
|
1087
|
+
meta.textContent = parts.join(' · ');
|
|
1088
|
+
item.appendChild(meta);
|
|
1089
|
+
|
|
1090
|
+
if (review.body) {
|
|
1091
|
+
const body = document.createElement('div');
|
|
1092
|
+
body.className = 'pr-status-note-body';
|
|
1093
|
+
body.textContent = review.body;
|
|
1094
|
+
item.appendChild(body);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
list.appendChild(item);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
container.appendChild(list);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (Array.isArray(pr.requestedReviewers) && pr.requestedReviewers.length > 0) {
|
|
1104
|
+
const requested = document.createElement('div');
|
|
1105
|
+
requested.className = 'pr-status-note-meta';
|
|
1106
|
+
requested.textContent = `Requested reviewers: ${pr.requestedReviewers.join(', ')}`;
|
|
1107
|
+
container.appendChild(requested);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
998
1110
|
function formatTime(ts) {
|
|
999
1111
|
try {
|
|
1000
1112
|
return new Date(ts).toLocaleTimeString([], {
|
|
@@ -2573,9 +2685,18 @@ function renderReview(jobs) {
|
|
|
2573
2685
|
els.reviewGeneralComment.disabled = !editable;
|
|
2574
2686
|
}
|
|
2575
2687
|
els.reviewPrSection.classList.toggle('hidden', !isOpened);
|
|
2688
|
+
if (isOpened) {
|
|
2689
|
+
if (els.reviewPrStatusLine) {
|
|
2690
|
+
els.reviewPrStatusLine.textContent = job.prStatus
|
|
2691
|
+
? jobPrStatusSummary(job)
|
|
2692
|
+
: 'GitHub status loading…';
|
|
2693
|
+
}
|
|
2694
|
+
renderPrStatusNotes(els.reviewPrStatusNotes, job.prStatus);
|
|
2695
|
+
} else {
|
|
2696
|
+
if (els.reviewPrStatusLine) els.reviewPrStatusLine.textContent = '';
|
|
2697
|
+
if (els.reviewPrStatusNotes) els.reviewPrStatusNotes.innerHTML = '';
|
|
2698
|
+
}
|
|
2576
2699
|
els.reviewTerminalSection.classList.toggle('hidden', !isTerminal);
|
|
2577
|
-
|
|
2578
|
-
const prMeta = resolvePrMeta(job);
|
|
2579
2700
|
if (document.activeElement !== els.prTitle && document.activeElement !== els.prBody) {
|
|
2580
2701
|
els.prTitle.value = prMeta.prTitle;
|
|
2581
2702
|
els.prBody.value = prMeta.prBody;
|
|
@@ -3793,12 +3914,11 @@ function knownToolsList(cfg = appConfig) {
|
|
|
3793
3914
|
}
|
|
3794
3915
|
|
|
3795
3916
|
/**
|
|
3796
|
-
* Switch Settings top-level tab (Ticket source / Authentication /
|
|
3797
|
-
* @param {'ticket' | 'auth' | '
|
|
3917
|
+
* Switch Settings top-level tab (Ticket source / Authentication / Configuration).
|
|
3918
|
+
* @param {'ticket' | 'auth' | 'config'} tab
|
|
3798
3919
|
*/
|
|
3799
3920
|
function setSettingsTab(tab) {
|
|
3800
|
-
const next =
|
|
3801
|
-
tab === 'auth' || tab === 'rules' || tab === 'config' ? tab : 'ticket';
|
|
3921
|
+
const next = tab === 'auth' || tab === 'config' ? tab : 'ticket';
|
|
3802
3922
|
settingsTab = next;
|
|
3803
3923
|
|
|
3804
3924
|
if (els.settingsTabs) {
|
|
@@ -4008,7 +4128,12 @@ function fillSettingsForm(cfg) {
|
|
|
4008
4128
|
els.settingsJiraRuleAction.value = action;
|
|
4009
4129
|
}
|
|
4010
4130
|
if (els.settingsJiraRuleStatus) {
|
|
4011
|
-
|
|
4131
|
+
fillStatusSelect(
|
|
4132
|
+
els.settingsJiraRuleStatus,
|
|
4133
|
+
jiraStatusOptions,
|
|
4134
|
+
jiraRule.targetStatus || '',
|
|
4135
|
+
{ hintEl: els.settingsJiraRuleStatusHint }
|
|
4136
|
+
);
|
|
4012
4137
|
}
|
|
4013
4138
|
if (els.settingsJiraRuleLabel) {
|
|
4014
4139
|
els.settingsJiraRuleLabel.value = jiraRule.label || '';
|
|
@@ -4025,7 +4150,12 @@ function fillSettingsForm(cfg) {
|
|
|
4025
4150
|
els.settingsGithubRuleAction.value = action;
|
|
4026
4151
|
}
|
|
4027
4152
|
if (els.settingsGithubRuleStatus) {
|
|
4028
|
-
|
|
4153
|
+
fillStatusSelect(
|
|
4154
|
+
els.settingsGithubRuleStatus,
|
|
4155
|
+
githubStatusOptions,
|
|
4156
|
+
ghRule.targetStatus || '',
|
|
4157
|
+
{ hintEl: els.settingsGithubRuleStatusHint }
|
|
4158
|
+
);
|
|
4029
4159
|
}
|
|
4030
4160
|
if (els.settingsGithubRuleLabel) {
|
|
4031
4161
|
els.settingsGithubRuleLabel.value = ghRule.label || '';
|
|
@@ -4097,6 +4227,131 @@ function fillSettingsForm(cfg) {
|
|
|
4097
4227
|
}
|
|
4098
4228
|
}
|
|
4099
4229
|
|
|
4230
|
+
/**
|
|
4231
|
+
* @param {HTMLSelectElement | null} selectEl
|
|
4232
|
+
* @param {Array<{ name: string, column?: string, board?: string, projectTitle?: string }>} statuses
|
|
4233
|
+
* @param {string} selected
|
|
4234
|
+
* @param {{ loading?: boolean, error?: string, empty?: string, hintEl?: HTMLElement | null, hintOk?: string }} [meta]
|
|
4235
|
+
*/
|
|
4236
|
+
function fillStatusSelect(selectEl, statuses, selected, meta = {}) {
|
|
4237
|
+
if (!selectEl) return;
|
|
4238
|
+
const current = String(selected || '').trim();
|
|
4239
|
+
selectEl.innerHTML = '';
|
|
4240
|
+
if (meta.loading) {
|
|
4241
|
+
selectEl.disabled = true;
|
|
4242
|
+
const opt = document.createElement('option');
|
|
4243
|
+
opt.value = current;
|
|
4244
|
+
opt.textContent = current ? `${current} (loading…)` : 'Loading statuses…';
|
|
4245
|
+
selectEl.appendChild(opt);
|
|
4246
|
+
selectEl.value = current;
|
|
4247
|
+
if (meta.hintEl) meta.hintEl.textContent = 'Fetching live statuses…';
|
|
4248
|
+
return;
|
|
4249
|
+
}
|
|
4250
|
+
selectEl.disabled = false;
|
|
4251
|
+
const list = Array.isArray(statuses) ? statuses : [];
|
|
4252
|
+
if (current && !list.some((s) => s.name === current)) {
|
|
4253
|
+
const saved = document.createElement('option');
|
|
4254
|
+
saved.value = current;
|
|
4255
|
+
saved.textContent = `${current} (saved)`;
|
|
4256
|
+
selectEl.appendChild(saved);
|
|
4257
|
+
}
|
|
4258
|
+
for (const st of list) {
|
|
4259
|
+
const name = String(st.name || '').trim();
|
|
4260
|
+
if (!name) continue;
|
|
4261
|
+
const opt = document.createElement('option');
|
|
4262
|
+
opt.value = name;
|
|
4263
|
+
const extra = st.column && st.column !== name
|
|
4264
|
+
? st.column
|
|
4265
|
+
: st.board || st.projectTitle || '';
|
|
4266
|
+
opt.textContent = extra ? `${name} — ${extra}` : name;
|
|
4267
|
+
selectEl.appendChild(opt);
|
|
4268
|
+
}
|
|
4269
|
+
if (!selectEl.options.length) {
|
|
4270
|
+
const opt = document.createElement('option');
|
|
4271
|
+
opt.value = current;
|
|
4272
|
+
opt.textContent = meta.error || meta.empty || 'No statuses found';
|
|
4273
|
+
selectEl.appendChild(opt);
|
|
4274
|
+
}
|
|
4275
|
+
if (current) selectEl.value = current;
|
|
4276
|
+
if (meta.hintEl) {
|
|
4277
|
+
if (meta.error) meta.hintEl.textContent = meta.error;
|
|
4278
|
+
else if (meta.hintOk) meta.hintEl.textContent = meta.hintOk;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
async function fetchJiraRuleStatuses() {
|
|
4283
|
+
const selected =
|
|
4284
|
+
els.settingsJiraRuleStatus?.value?.trim() ||
|
|
4285
|
+
appConfig.jiraRules?.afterPrOpened?.targetStatus ||
|
|
4286
|
+
'';
|
|
4287
|
+
fillStatusSelect(els.settingsJiraRuleStatus, jiraStatusOptions, selected, {
|
|
4288
|
+
loading: true,
|
|
4289
|
+
hintEl: els.settingsJiraRuleStatusHint,
|
|
4290
|
+
});
|
|
4291
|
+
try {
|
|
4292
|
+
const res = await fetch('/api/jira/statuses');
|
|
4293
|
+
const data = await readJson(res);
|
|
4294
|
+
if (!res.ok || !data.ok) {
|
|
4295
|
+
fillStatusSelect(els.settingsJiraRuleStatus, jiraStatusOptions, selected, {
|
|
4296
|
+
error: data.error || `Failed to load Jira statuses (HTTP ${res.status})`,
|
|
4297
|
+
hintEl: els.settingsJiraRuleStatusHint,
|
|
4298
|
+
});
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
jiraStatusOptions = Array.isArray(data.statuses) ? data.statuses : [];
|
|
4302
|
+
const source = data.source === 'board' ? 'Jira board' : 'Jira workflow catalog';
|
|
4303
|
+
fillStatusSelect(els.settingsJiraRuleStatus, jiraStatusOptions, selected, {
|
|
4304
|
+
hintEl: els.settingsJiraRuleStatusHint,
|
|
4305
|
+
hintOk: `Live statuses from ${source}. Must match a reachable workflow status.`,
|
|
4306
|
+
});
|
|
4307
|
+
} catch (err) {
|
|
4308
|
+
fillStatusSelect(els.settingsJiraRuleStatus, jiraStatusOptions, selected, {
|
|
4309
|
+
error: err.message || 'Failed to load Jira statuses',
|
|
4310
|
+
hintEl: els.settingsJiraRuleStatusHint,
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
async function fetchGithubRuleStatuses() {
|
|
4316
|
+
const selected =
|
|
4317
|
+
els.settingsGithubRuleStatus?.value?.trim() ||
|
|
4318
|
+
appConfig.githubRules?.afterPrOpened?.targetStatus ||
|
|
4319
|
+
'';
|
|
4320
|
+
fillStatusSelect(els.settingsGithubRuleStatus, githubStatusOptions, selected, {
|
|
4321
|
+
loading: true,
|
|
4322
|
+
hintEl: els.settingsGithubRuleStatusHint,
|
|
4323
|
+
});
|
|
4324
|
+
try {
|
|
4325
|
+
const res = await fetch('/api/github/statuses');
|
|
4326
|
+
const data = await readJson(res);
|
|
4327
|
+
if (!res.ok || !data.ok) {
|
|
4328
|
+
fillStatusSelect(els.settingsGithubRuleStatus, githubStatusOptions, selected, {
|
|
4329
|
+
error: data.error || `Failed to load GitHub statuses (HTTP ${res.status})`,
|
|
4330
|
+
hintEl: els.settingsGithubRuleStatusHint,
|
|
4331
|
+
});
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
4334
|
+
githubStatusOptions = Array.isArray(data.statuses) ? data.statuses : [];
|
|
4335
|
+
const hintOk =
|
|
4336
|
+
data.source === 'project'
|
|
4337
|
+
? 'Live GitHub Project Status options for this repo.'
|
|
4338
|
+
: 'No Project Status field — listing repo labels instead.';
|
|
4339
|
+
fillStatusSelect(els.settingsGithubRuleStatus, githubStatusOptions, selected, {
|
|
4340
|
+
hintEl: els.settingsGithubRuleStatusHint,
|
|
4341
|
+
hintOk,
|
|
4342
|
+
});
|
|
4343
|
+
} catch (err) {
|
|
4344
|
+
fillStatusSelect(els.settingsGithubRuleStatus, githubStatusOptions, selected, {
|
|
4345
|
+
error: err.message || 'Failed to load GitHub statuses',
|
|
4346
|
+
hintEl: els.settingsGithubRuleStatusHint,
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
function fetchRuleStatuses() {
|
|
4352
|
+
return Promise.all([fetchJiraRuleStatuses(), fetchGithubRuleStatuses()]);
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4100
4355
|
/**
|
|
4101
4356
|
* Show status / label fields based on the selected post-PR action.
|
|
4102
4357
|
* @param {'jira' | 'github'} source
|
|
@@ -4570,7 +4825,7 @@ els.settingsTabs?.addEventListener('click', (e) => {
|
|
|
4570
4825
|
const btn = e.target.closest('[data-settings-tab]');
|
|
4571
4826
|
if (!btn?.dataset.settingsTab) return;
|
|
4572
4827
|
setSettingsTab(
|
|
4573
|
-
/** @type {'ticket' | 'auth' | '
|
|
4828
|
+
/** @type {'ticket' | 'auth' | 'config'} */ (btn.dataset.settingsTab)
|
|
4574
4829
|
);
|
|
4575
4830
|
});
|
|
4576
4831
|
|
|
@@ -4594,7 +4849,7 @@ els.settingsTabs?.addEventListener('keydown', (e) => {
|
|
|
4594
4849
|
e.preventDefault();
|
|
4595
4850
|
const nextTab = tabs[nextIdx];
|
|
4596
4851
|
setSettingsTab(
|
|
4597
|
-
/** @type {'ticket' | 'auth' | '
|
|
4852
|
+
/** @type {'ticket' | 'auth' | 'config'} */ (nextTab.dataset.settingsTab)
|
|
4598
4853
|
);
|
|
4599
4854
|
nextTab.focus();
|
|
4600
4855
|
});
|
package/public/index.html
CHANGED
|
@@ -352,6 +352,8 @@
|
|
|
352
352
|
|
|
353
353
|
<div id="review-pr-section" class="card card-success hidden">
|
|
354
354
|
<p class="pr-opened-line">PR opened: <a id="review-pr-link" href="#" target="_blank" rel="noopener"></a></p>
|
|
355
|
+
<p id="review-pr-status-line" class="field-hint"></p>
|
|
356
|
+
<div id="review-pr-status-notes" class="pr-status-notes"></div>
|
|
355
357
|
<div class="actions">
|
|
356
358
|
<button id="review-pr-clear-btn" type="button" class="btn btn-secondary btn-sm">Clear from history</button>
|
|
357
359
|
</div>
|
|
@@ -431,6 +433,7 @@
|
|
|
431
433
|
aria-selected="false"
|
|
432
434
|
tabindex="-1"
|
|
433
435
|
>Authentication</button>
|
|
436
|
+
<!--
|
|
434
437
|
<button
|
|
435
438
|
type="button"
|
|
436
439
|
class="settings-tab"
|
|
@@ -441,6 +444,7 @@
|
|
|
441
444
|
aria-selected="false"
|
|
442
445
|
tabindex="-1"
|
|
443
446
|
>Rules</button>
|
|
447
|
+
-->
|
|
444
448
|
<button
|
|
445
449
|
type="button"
|
|
446
450
|
class="settings-tab"
|
|
@@ -571,6 +575,7 @@
|
|
|
571
575
|
</div>
|
|
572
576
|
</div>
|
|
573
577
|
|
|
578
|
+
<!--
|
|
574
579
|
<div
|
|
575
580
|
class="settings-panel hidden"
|
|
576
581
|
role="tabpanel"
|
|
@@ -604,9 +609,9 @@
|
|
|
604
609
|
</select>
|
|
605
610
|
</div>
|
|
606
611
|
<div class="settings-field" id="settings-jira-rule-status-field">
|
|
607
|
-
<label class="field-label" for="settings-jira-rule-status">Target status
|
|
608
|
-
<
|
|
609
|
-
<p class="field-hint">
|
|
612
|
+
<label class="field-label" for="settings-jira-rule-status">Target status</label>
|
|
613
|
+
<select id="settings-jira-rule-status" class="input" name="jiraRuleTargetStatus"></select>
|
|
614
|
+
<p class="field-hint" id="settings-jira-rule-status-hint">Live statuses from your Jira kanban/scrum boards. Must match a reachable workflow status.</p>
|
|
610
615
|
</div>
|
|
611
616
|
<div class="settings-field" id="settings-jira-rule-label-field">
|
|
612
617
|
<label class="field-label" for="settings-jira-rule-label">Label</label>
|
|
@@ -619,7 +624,7 @@
|
|
|
619
624
|
<div id="github-rules-panel" class="rules-block">
|
|
620
625
|
<div class="field-label">GitHub Issues — after PR opened</div>
|
|
621
626
|
<p class="field-hint">
|
|
622
|
-
|
|
627
|
+
Move status uses a live board Status when the repo has a GitHub Project; otherwise a matching issue label. Add label uses the labels API. Close closes the issue.
|
|
623
628
|
</p>
|
|
624
629
|
<div class="settings-grid">
|
|
625
630
|
<div class="settings-field settings-field-full">
|
|
@@ -638,9 +643,9 @@
|
|
|
638
643
|
</select>
|
|
639
644
|
</div>
|
|
640
645
|
<div class="settings-field" id="settings-github-rule-status-field">
|
|
641
|
-
<label class="field-label" for="settings-github-rule-status">Target status
|
|
642
|
-
<
|
|
643
|
-
<p class="field-hint">
|
|
646
|
+
<label class="field-label" for="settings-github-rule-status">Target status</label>
|
|
647
|
+
<select id="settings-github-rule-status" class="input" name="githubRuleTargetStatus"></select>
|
|
648
|
+
<p class="field-hint" id="settings-github-rule-status-hint">Live statuses from GitHub Project v2 or labels. Must match a reachable board status / label.</p>
|
|
644
649
|
</div>
|
|
645
650
|
<div class="settings-field" id="settings-github-rule-label-field">
|
|
646
651
|
<label class="field-label" for="settings-github-rule-label">Label</label>
|
|
@@ -651,6 +656,8 @@
|
|
|
651
656
|
</div>
|
|
652
657
|
</div>
|
|
653
658
|
</div>
|
|
659
|
+
-->
|
|
660
|
+
</div>
|
|
654
661
|
|
|
655
662
|
<div
|
|
656
663
|
class="settings-panel hidden"
|
package/public/styles.css
CHANGED
|
@@ -1594,6 +1594,43 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
1594
1594
|
margin-bottom: 12px;
|
|
1595
1595
|
}
|
|
1596
1596
|
|
|
1597
|
+
.pr-status-notes {
|
|
1598
|
+
display: grid;
|
|
1599
|
+
gap: 10px;
|
|
1600
|
+
margin: 0 0 12px;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
.pr-status-notes-title {
|
|
1604
|
+
font-size: 12px;
|
|
1605
|
+
font-weight: 700;
|
|
1606
|
+
letter-spacing: 0.04em;
|
|
1607
|
+
text-transform: uppercase;
|
|
1608
|
+
color: var(--text-muted);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.pr-status-note-list {
|
|
1612
|
+
display: grid;
|
|
1613
|
+
gap: 8px;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
.pr-status-note {
|
|
1617
|
+
padding: 10px 12px;
|
|
1618
|
+
border: 1px solid var(--border-soft);
|
|
1619
|
+
border-radius: 10px;
|
|
1620
|
+
background: var(--surface-2);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.pr-status-note-meta {
|
|
1624
|
+
font-size: 12px;
|
|
1625
|
+
color: var(--text-muted);
|
|
1626
|
+
margin-bottom: 4px;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
.pr-status-note-body {
|
|
1630
|
+
white-space: pre-wrap;
|
|
1631
|
+
line-height: 1.45;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1597
1634
|
/* —— Review file selection —— */
|
|
1598
1635
|
.review-files-header {
|
|
1599
1636
|
display: flex;
|
package/src/afterPrRules.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { normalizeGithubRules, normalizeJiraRules } from './config.js';
|
|
7
|
-
import { addIssueLabel, closeIssue } from './github.js';
|
|
7
|
+
import { addIssueLabel, closeIssue, setGithubIssueProjectStatus } from './github.js';
|
|
8
|
+
import { originRemoteInfo } from './gh-auth.js';
|
|
8
9
|
import {
|
|
9
10
|
addJiraIssueLabel,
|
|
10
11
|
closeJiraIssue,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
* closeJiraIssue?: typeof closeJiraIssue,
|
|
31
32
|
* addIssueLabel?: typeof addIssueLabel,
|
|
32
33
|
* closeIssue?: typeof closeIssue,
|
|
34
|
+
* setGithubIssueProjectStatus?: typeof setGithubIssueProjectStatus,
|
|
33
35
|
* resolveJiraCredentials?: typeof resolveJiraCredentials,
|
|
34
36
|
* },
|
|
35
37
|
* }} params
|
|
@@ -47,6 +49,7 @@ export async function applyAfterPrOpenedRules({
|
|
|
47
49
|
const doCloseJira = deps.closeJiraIssue || closeJiraIssue;
|
|
48
50
|
const doAddLabel = deps.addIssueLabel || addIssueLabel;
|
|
49
51
|
const doClose = deps.closeIssue || closeIssue;
|
|
52
|
+
const doSetProjectStatus = deps.setGithubIssueProjectStatus || setGithubIssueProjectStatus;
|
|
50
53
|
const doResolveCreds = deps.resolveJiraCredentials || resolveJiraCredentials;
|
|
51
54
|
|
|
52
55
|
const source = job.ticketSource === 'jira' || job.jiraKey ? 'jira' : 'github';
|
|
@@ -133,9 +136,22 @@ export async function applyAfterPrOpenedRules({
|
|
|
133
136
|
appendLog(job, 'warn', msg);
|
|
134
137
|
return { applied: false, message: msg };
|
|
135
138
|
}
|
|
136
|
-
|
|
139
|
+
const originUrl = originRemoteInfo(repoRoot).url;
|
|
140
|
+
const project = await doSetProjectStatus({
|
|
141
|
+
issueNumber: job.issueNumber,
|
|
142
|
+
statusName: status,
|
|
143
|
+
cwd,
|
|
144
|
+
originUrl,
|
|
145
|
+
});
|
|
146
|
+
if (project?.ok) {
|
|
147
|
+
const board = project.projectTitle ? ` on "${project.projectTitle}"` : '';
|
|
148
|
+
const msg = `Moved GitHub issue #${job.issueNumber} status to "${project.statusName}"${board} after PR opened`;
|
|
149
|
+
appendLog(job, 'info', msg);
|
|
150
|
+
return { applied: true, message: msg };
|
|
151
|
+
}
|
|
137
152
|
await doAddLabel(job.issueNumber, status, cwd);
|
|
138
|
-
const
|
|
153
|
+
const fallback = project?.error ? ` (${project.error})` : '';
|
|
154
|
+
const msg = `Moved GitHub issue #${job.issueNumber} status to "${status}" (label)${fallback} after PR opened`;
|
|
139
155
|
appendLog(job, 'info', msg);
|
|
140
156
|
return { applied: true, message: msg };
|
|
141
157
|
}
|
package/src/config.js
CHANGED
|
@@ -31,7 +31,7 @@ export const LLM_PROVIDERS = /** @type {const} */ (['claude', 'openrouter']);
|
|
|
31
31
|
/**
|
|
32
32
|
* Shared post-PR actions for Jira and GitHub Issues rules.
|
|
33
33
|
* - Jira `set_status` / `close_issue`: workflow transitions (close → Done-like status).
|
|
34
|
-
* - GitHub `set_status`:
|
|
34
|
+
* - GitHub `set_status`: GitHub Projects v2 Status field, else a label named after `targetStatus`.
|
|
35
35
|
* - Both `add_label`: add the configured label name.
|
|
36
36
|
*/
|
|
37
37
|
export const AFTER_PR_ACTIONS = /** @type {const} */ ([
|