npm-cli-gh-issue-preparator 1.32.1 → 1.33.0
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/.github/CODEOWNERS +6 -2
- package/.github/copilot-instructions.md +6 -0
- package/.github/dependabot.yml +17 -0
- package/.github/instructions/code-review.instructions.md +7 -0
- package/.github/workflows/api-created_issue_pr.yml +16 -1
- package/.github/workflows/close-manual-prs.yml +46 -0
- package/.github/workflows/commit-lint.yml +16 -3
- package/.github/workflows/configs/commitlint.config.js +5 -0
- package/.github/workflows/create-pr.yml +19 -17
- package/.github/workflows/secret-scan.yml +51 -0
- package/.github/workflows/umino-project.yml +106 -27
- package/.gitleaks.toml +9 -0
- package/.prettierignore +1 -0
- package/CHANGELOG.md +7 -0
- package/bin/adapter/entry-points/cli/index.js +10 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/domain/usecases/StaleTmuxSessionKillUseCase.js +80 -0
- package/bin/domain/usecases/StaleTmuxSessionKillUseCase.js.map +1 -0
- package/package.json +2 -2
- package/renovate.json +5 -0
- package/src/adapter/entry-points/cli/index.test.ts +58 -0
- package/src/adapter/entry-points/cli/index.ts +21 -0
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +262 -0
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.ts +123 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/domain/usecases/StaleTmuxSessionKillUseCase.d.ts +25 -0
- package/types/domain/usecases/StaleTmuxSessionKillUseCase.d.ts.map +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StaleTmuxSessionKillUseCase = exports.DEFAULT_IDLE_THRESHOLD_SECONDS = exports.DEFAULT_EXCLUDED_STATUS = void 0;
|
|
4
|
+
exports.DEFAULT_EXCLUDED_STATUS = 'In Tmux by human';
|
|
5
|
+
exports.DEFAULT_IDLE_THRESHOLD_SECONDS = 24 * 60 * 60;
|
|
6
|
+
class StaleTmuxSessionKillUseCase {
|
|
7
|
+
constructor(projectRepository, issueRepository, localCommandRunner) {
|
|
8
|
+
this.projectRepository = projectRepository;
|
|
9
|
+
this.issueRepository = issueRepository;
|
|
10
|
+
this.localCommandRunner = localCommandRunner;
|
|
11
|
+
this.run = async (params) => {
|
|
12
|
+
const liveSessions = await this.listLiveSessions();
|
|
13
|
+
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
14
|
+
const openIssues = await this.issueRepository.getAllOpened(project);
|
|
15
|
+
const issueBySessionName = new Map();
|
|
16
|
+
for (const issue of openIssues) {
|
|
17
|
+
issueBySessionName.set(this.deriveSessionName(issue.url), issue);
|
|
18
|
+
}
|
|
19
|
+
const nowEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
20
|
+
const killCandidates = [];
|
|
21
|
+
for (const session of liveSessions) {
|
|
22
|
+
const reason = this.evaluateKillReason(session, issueBySessionName.get(session.sessionName) ?? null, nowEpochSeconds, params.excludedStatus, params.idleThresholdSeconds);
|
|
23
|
+
if (reason !== null) {
|
|
24
|
+
killCandidates.push({ sessionName: session.sessionName, reason });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
console.log(`Stale tmux session cleanup: ${killCandidates.length} kill candidate(s) of ${liveSessions.length} live session(s).`);
|
|
28
|
+
for (const candidate of killCandidates) {
|
|
29
|
+
console.log(`Kill candidate: ${candidate.sessionName} (${candidate.reason})`);
|
|
30
|
+
}
|
|
31
|
+
for (const candidate of killCandidates) {
|
|
32
|
+
await this.localCommandRunner.runCommand('tmux', [
|
|
33
|
+
'kill-session',
|
|
34
|
+
'-t',
|
|
35
|
+
candidate.sessionName,
|
|
36
|
+
]);
|
|
37
|
+
console.log(`Killed tmux session: ${candidate.sessionName} (${candidate.reason})`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
this.listLiveSessions = async () => {
|
|
41
|
+
const result = await this.localCommandRunner.runCommand('tmux', [
|
|
42
|
+
'list-sessions',
|
|
43
|
+
'-F',
|
|
44
|
+
'#{session_name} #{session_activity}',
|
|
45
|
+
]);
|
|
46
|
+
return result.stdout
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((line) => line.trim())
|
|
49
|
+
.filter((line) => line.length > 0)
|
|
50
|
+
.map((line) => {
|
|
51
|
+
const separatorIndex = line.lastIndexOf(' ');
|
|
52
|
+
const sessionName = line.slice(0, separatorIndex);
|
|
53
|
+
const activityEpochSeconds = Number(line.slice(separatorIndex + 1));
|
|
54
|
+
return { sessionName, activityEpochSeconds };
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
this.deriveSessionName = (issueUrl) => issueUrl.replace(/[.:]/g, '_');
|
|
58
|
+
this.evaluateKillReason = (session, issue, nowEpochSeconds, excludedStatus, idleThresholdSeconds) => {
|
|
59
|
+
if (issue !== null) {
|
|
60
|
+
if (issue.status !== excludedStatus) {
|
|
61
|
+
return `mapped to open issue ${issue.url} with status "${issue.status ?? 'null'}" which is not the excluded status "${excludedStatus}"`;
|
|
62
|
+
}
|
|
63
|
+
if (issue.nextActionDate !== null) {
|
|
64
|
+
return `mapped to open issue ${issue.url} which has a next action date set`;
|
|
65
|
+
}
|
|
66
|
+
if (issue.nextActionHour !== null) {
|
|
67
|
+
return `mapped to open issue ${issue.url} which has a next action hour set`;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const idleSeconds = nowEpochSeconds - session.activityEpochSeconds;
|
|
72
|
+
if (idleSeconds >= idleThresholdSeconds) {
|
|
73
|
+
return `maps to no open issue and has been idle for ${idleSeconds} seconds (threshold ${idleThresholdSeconds} seconds)`;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.StaleTmuxSessionKillUseCase = StaleTmuxSessionKillUseCase;
|
|
80
|
+
//# sourceMappingURL=StaleTmuxSessionKillUseCase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StaleTmuxSessionKillUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/StaleTmuxSessionKillUseCase.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "npm-cli-gh-issue-preparator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
53
53
|
"@typescript-eslint/parser": "^7.2.0",
|
|
54
54
|
"check-node-version": "^4.2.1",
|
|
55
|
-
"commitlint": "^
|
|
55
|
+
"commitlint": "^21.0.0",
|
|
56
56
|
"conventional-changelog-conventionalcommits": "^9.0.0",
|
|
57
57
|
"eslint": "^8.57.0",
|
|
58
58
|
"eslint-plugin-import": "^2.29.1",
|
package/renovate.json
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// DO NOT EDIT DIRECTLY.
|
|
2
|
+
// This file is auto-synchronized from HiromiShikata/repositories-management.
|
|
3
|
+
// Direct edits in downstream repositories will be overwritten by the next sync.
|
|
4
|
+
// Update the source file in HiromiShikata/repositories-management instead.
|
|
1
5
|
{
|
|
2
6
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
7
|
"extends": ["config:base"],
|
|
@@ -10,6 +14,7 @@
|
|
|
10
14
|
"dependencyDashboard": false,
|
|
11
15
|
"branchConcurrentLimit": 2,
|
|
12
16
|
"ignorePaths": ["**/generated/*", "**/_gen/*"],
|
|
17
|
+
"osvVulnerabilityAlerts": true,
|
|
13
18
|
"packageRules": [
|
|
14
19
|
{
|
|
15
20
|
"matchPackageNames": ["eslint"],
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
mergeConfigs,
|
|
9
9
|
} from './index';
|
|
10
10
|
import { StartPreparationUseCase } from '../../../domain/usecases/StartPreparationUseCase';
|
|
11
|
+
import { StaleTmuxSessionKillUseCase } from '../../../domain/usecases/StaleTmuxSessionKillUseCase';
|
|
11
12
|
import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
|
|
12
13
|
|
|
13
14
|
jest.mock('../../../domain/usecases/StartPreparationUseCase');
|
|
15
|
+
jest.mock('../../../domain/usecases/StaleTmuxSessionKillUseCase');
|
|
14
16
|
jest.mock('../../../domain/usecases/NotifyFinishedIssuePreparationUseCase');
|
|
15
17
|
jest.mock('../../repositories/TowerDefenceIssueRepository', () => ({
|
|
16
18
|
TowerDefenceIssueRepository: jest.fn().mockImplementation(() => ({
|
|
@@ -420,6 +422,62 @@ defaultAgentName: 'case-test-agent'
|
|
|
420
422
|
});
|
|
421
423
|
|
|
422
424
|
describe('startDaemon', () => {
|
|
425
|
+
const setupStaleTmuxMock = (): jest.Mock<
|
|
426
|
+
Promise<void>,
|
|
427
|
+
Parameters<StaleTmuxSessionKillUseCase['run']>
|
|
428
|
+
> => {
|
|
429
|
+
const staleTmuxRun = jest.fn<
|
|
430
|
+
Promise<void>,
|
|
431
|
+
Parameters<StaleTmuxSessionKillUseCase['run']>
|
|
432
|
+
>(() => Promise.resolve());
|
|
433
|
+
const MockedStaleTmuxSessionKillUseCase = jest.mocked(
|
|
434
|
+
StaleTmuxSessionKillUseCase,
|
|
435
|
+
);
|
|
436
|
+
MockedStaleTmuxSessionKillUseCase.mockImplementation(function (
|
|
437
|
+
this: StaleTmuxSessionKillUseCase,
|
|
438
|
+
) {
|
|
439
|
+
this.run = staleTmuxRun;
|
|
440
|
+
return this;
|
|
441
|
+
});
|
|
442
|
+
return staleTmuxRun;
|
|
443
|
+
};
|
|
444
|
+
beforeEach(() => {
|
|
445
|
+
setupStaleTmuxMock();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should run stale tmux session cleanup after preparation', async () => {
|
|
449
|
+
const mockStaleTmuxRun = setupStaleTmuxMock();
|
|
450
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
451
|
+
const MockedStartPreparationUseCase = jest.mocked(
|
|
452
|
+
StartPreparationUseCase,
|
|
453
|
+
);
|
|
454
|
+
MockedStartPreparationUseCase.mockImplementation(function (
|
|
455
|
+
this: StartPreparationUseCase,
|
|
456
|
+
) {
|
|
457
|
+
this.run = mockRun;
|
|
458
|
+
return this;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
await program.parseAsync([
|
|
462
|
+
'node',
|
|
463
|
+
'test',
|
|
464
|
+
'startDaemon',
|
|
465
|
+
'--configFilePath',
|
|
466
|
+
configFilePath,
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
expect(mockStaleTmuxRun).toHaveBeenCalledTimes(1);
|
|
470
|
+
expect(mockStaleTmuxRun).toHaveBeenCalledWith(
|
|
471
|
+
expect.objectContaining({
|
|
472
|
+
projectUrl: 'https://github.com/test/project',
|
|
473
|
+
excludedStatus: 'In Tmux by human',
|
|
474
|
+
idleThresholdSeconds: 86400,
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
477
|
+
const staleTmuxCallArgument = mockStaleTmuxRun.mock.calls[0][0];
|
|
478
|
+
expect(staleTmuxCallArgument.now).toBeInstanceOf(Date);
|
|
479
|
+
});
|
|
480
|
+
|
|
423
481
|
it('should read parameters from config file', async () => {
|
|
424
482
|
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
425
483
|
const MockedStartPreparationUseCase = jest.mocked(
|
|
@@ -6,6 +6,11 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as yaml from 'js-yaml';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { StartPreparationUseCase } from '../../../domain/usecases/StartPreparationUseCase';
|
|
9
|
+
import {
|
|
10
|
+
StaleTmuxSessionKillUseCase,
|
|
11
|
+
DEFAULT_EXCLUDED_STATUS,
|
|
12
|
+
DEFAULT_IDLE_THRESHOLD_SECONDS,
|
|
13
|
+
} from '../../../domain/usecases/StaleTmuxSessionKillUseCase';
|
|
9
14
|
import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
|
|
10
15
|
import { TowerDefenceIssueRepository } from '../../repositories/TowerDefenceIssueRepository';
|
|
11
16
|
import { GraphqlIssueRepository } from '../../repositories/GraphqlIssueRepository';
|
|
@@ -455,6 +460,22 @@ program
|
|
|
455
460
|
utilizationPercentageThreshold,
|
|
456
461
|
allowedIssueAuthors,
|
|
457
462
|
});
|
|
463
|
+
|
|
464
|
+
const staleTmuxSessionKillUseCase = new StaleTmuxSessionKillUseCase(
|
|
465
|
+
projectRepository,
|
|
466
|
+
{
|
|
467
|
+
getAllOpened: towerDefenceIssueRepository.getAllOpened.bind(
|
|
468
|
+
towerDefenceIssueRepository,
|
|
469
|
+
),
|
|
470
|
+
},
|
|
471
|
+
localCommandRunner,
|
|
472
|
+
);
|
|
473
|
+
await staleTmuxSessionKillUseCase.run({
|
|
474
|
+
projectUrl,
|
|
475
|
+
excludedStatus: DEFAULT_EXCLUDED_STATUS,
|
|
476
|
+
idleThresholdSeconds: DEFAULT_IDLE_THRESHOLD_SECONDS,
|
|
477
|
+
now: new Date(),
|
|
478
|
+
});
|
|
458
479
|
});
|
|
459
480
|
|
|
460
481
|
program
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StaleTmuxSessionKillUseCase,
|
|
3
|
+
DEFAULT_EXCLUDED_STATUS,
|
|
4
|
+
DEFAULT_IDLE_THRESHOLD_SECONDS,
|
|
5
|
+
} from './StaleTmuxSessionKillUseCase';
|
|
6
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
7
|
+
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
8
|
+
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
9
|
+
import { Issue } from '../entities/Issue';
|
|
10
|
+
import { Project } from '../entities/Project';
|
|
11
|
+
|
|
12
|
+
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
13
|
+
|
|
14
|
+
const createMockProject = (): Project => ({
|
|
15
|
+
id: 'project-1',
|
|
16
|
+
url: 'https://github.com/users/user/projects/1',
|
|
17
|
+
databaseId: 1,
|
|
18
|
+
name: 'Test Project',
|
|
19
|
+
readme: null,
|
|
20
|
+
status: {
|
|
21
|
+
name: 'Status',
|
|
22
|
+
fieldId: 'status-field-id',
|
|
23
|
+
statuses: [],
|
|
24
|
+
},
|
|
25
|
+
nextActionDate: null,
|
|
26
|
+
nextActionHour: null,
|
|
27
|
+
story: null,
|
|
28
|
+
remainingEstimationMinutes: null,
|
|
29
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
30
|
+
completionDate50PercentConfidence: null,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
34
|
+
nameWithOwner: 'user/repo',
|
|
35
|
+
number: 1,
|
|
36
|
+
title: 'Test Issue',
|
|
37
|
+
state: 'OPEN',
|
|
38
|
+
status: DEFAULT_EXCLUDED_STATUS,
|
|
39
|
+
story: null,
|
|
40
|
+
nextActionDate: null,
|
|
41
|
+
nextActionHour: null,
|
|
42
|
+
estimationMinutes: null,
|
|
43
|
+
dependedIssueUrls: [],
|
|
44
|
+
completionDate50PercentConfidence: null,
|
|
45
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
46
|
+
assignees: [],
|
|
47
|
+
labels: [],
|
|
48
|
+
org: 'user',
|
|
49
|
+
repo: 'repo',
|
|
50
|
+
body: '',
|
|
51
|
+
itemId: 'item-1',
|
|
52
|
+
isPr: false,
|
|
53
|
+
isInProgress: false,
|
|
54
|
+
isClosed: false,
|
|
55
|
+
createdAt: new Date(),
|
|
56
|
+
author: 'testuser',
|
|
57
|
+
...overrides,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const sessionNameOf = (issueUrl: string): string =>
|
|
61
|
+
issueUrl.replace(/[.:]/g, '_');
|
|
62
|
+
|
|
63
|
+
describe('StaleTmuxSessionKillUseCase', () => {
|
|
64
|
+
let useCase: StaleTmuxSessionKillUseCase;
|
|
65
|
+
let mockProjectRepository: Mocked<Pick<ProjectRepository, 'getByUrl'>>;
|
|
66
|
+
let mockIssueRepository: Mocked<Pick<IssueRepository, 'getAllOpened'>>;
|
|
67
|
+
let mockLocalCommandRunner: Mocked<Pick<LocalCommandRunner, 'runCommand'>>;
|
|
68
|
+
let mockProject: Project;
|
|
69
|
+
const now = new Date('2026-06-26T00:00:00Z');
|
|
70
|
+
const nowEpochSeconds = Math.floor(now.getTime() / 1000);
|
|
71
|
+
|
|
72
|
+
const runParams = {
|
|
73
|
+
projectUrl: 'https://github.com/user/repo',
|
|
74
|
+
excludedStatus: DEFAULT_EXCLUDED_STATUS,
|
|
75
|
+
idleThresholdSeconds: DEFAULT_IDLE_THRESHOLD_SECONDS,
|
|
76
|
+
now,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const setLiveSessions = (lines: string[]): void => {
|
|
80
|
+
mockLocalCommandRunner.runCommand.mockResolvedValueOnce({
|
|
81
|
+
stdout: lines.join('\n'),
|
|
82
|
+
stderr: '',
|
|
83
|
+
exitCode: 0,
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
jest.resetAllMocks();
|
|
89
|
+
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
90
|
+
mockProject = createMockProject();
|
|
91
|
+
mockProjectRepository = {
|
|
92
|
+
getByUrl: jest.fn().mockResolvedValue(mockProject),
|
|
93
|
+
};
|
|
94
|
+
mockIssueRepository = {
|
|
95
|
+
getAllOpened: jest.fn().mockResolvedValue([]),
|
|
96
|
+
};
|
|
97
|
+
mockLocalCommandRunner = {
|
|
98
|
+
runCommand: jest.fn().mockResolvedValue({
|
|
99
|
+
stdout: '',
|
|
100
|
+
stderr: '',
|
|
101
|
+
exitCode: 0,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
useCase = new StaleTmuxSessionKillUseCase(
|
|
105
|
+
mockProjectRepository,
|
|
106
|
+
mockIssueRepository,
|
|
107
|
+
mockLocalCommandRunner,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('exposes the excluded status and idle threshold as named constants', () => {
|
|
112
|
+
expect(DEFAULT_EXCLUDED_STATUS).toBe('In Tmux by human');
|
|
113
|
+
expect(DEFAULT_IDLE_THRESHOLD_SECONDS).toBe(86400);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('lists live sessions via the local command runner', async () => {
|
|
117
|
+
setLiveSessions([]);
|
|
118
|
+
await useCase.run(runParams);
|
|
119
|
+
expect(mockLocalCommandRunner.runCommand).toHaveBeenCalledWith('tmux', [
|
|
120
|
+
'list-sessions',
|
|
121
|
+
'-F',
|
|
122
|
+
'#{session_name} #{session_activity}',
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('kills a session mapping to an open issue whose status is not the excluded status', async () => {
|
|
127
|
+
const issueUrl = 'https://github.com/user/repo/issues/10';
|
|
128
|
+
const sessionName = sessionNameOf(issueUrl);
|
|
129
|
+
setLiveSessions([`${sessionName} ${nowEpochSeconds}`]);
|
|
130
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([
|
|
131
|
+
createMockIssue({ url: issueUrl, status: 'In Progress' }),
|
|
132
|
+
]);
|
|
133
|
+
await useCase.run(runParams);
|
|
134
|
+
expect(mockLocalCommandRunner.runCommand).toHaveBeenCalledWith('tmux', [
|
|
135
|
+
'kill-session',
|
|
136
|
+
'-t',
|
|
137
|
+
sessionName,
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('kills a session mapping to an open issue whose status is null', async () => {
|
|
142
|
+
const issueUrl = 'https://github.com/user/repo/issues/11';
|
|
143
|
+
const sessionName = sessionNameOf(issueUrl);
|
|
144
|
+
setLiveSessions([`${sessionName} ${nowEpochSeconds}`]);
|
|
145
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([
|
|
146
|
+
createMockIssue({ url: issueUrl, status: null }),
|
|
147
|
+
]);
|
|
148
|
+
await useCase.run(runParams);
|
|
149
|
+
expect(mockLocalCommandRunner.runCommand).toHaveBeenCalledWith('tmux', [
|
|
150
|
+
'kill-session',
|
|
151
|
+
'-t',
|
|
152
|
+
sessionName,
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('kills an excluded-status session that has a next action date set', async () => {
|
|
157
|
+
const issueUrl = 'https://github.com/user/repo/issues/12';
|
|
158
|
+
const sessionName = sessionNameOf(issueUrl);
|
|
159
|
+
setLiveSessions([`${sessionName} ${nowEpochSeconds}`]);
|
|
160
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([
|
|
161
|
+
createMockIssue({
|
|
162
|
+
url: issueUrl,
|
|
163
|
+
status: DEFAULT_EXCLUDED_STATUS,
|
|
164
|
+
nextActionDate: new Date('2026-06-27T00:00:00Z'),
|
|
165
|
+
}),
|
|
166
|
+
]);
|
|
167
|
+
await useCase.run(runParams);
|
|
168
|
+
expect(mockLocalCommandRunner.runCommand).toHaveBeenCalledWith('tmux', [
|
|
169
|
+
'kill-session',
|
|
170
|
+
'-t',
|
|
171
|
+
sessionName,
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('kills an excluded-status session that has a next action hour set', async () => {
|
|
176
|
+
const issueUrl = 'https://github.com/user/repo/issues/13';
|
|
177
|
+
const sessionName = sessionNameOf(issueUrl);
|
|
178
|
+
setLiveSessions([`${sessionName} ${nowEpochSeconds}`]);
|
|
179
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([
|
|
180
|
+
createMockIssue({
|
|
181
|
+
url: issueUrl,
|
|
182
|
+
status: DEFAULT_EXCLUDED_STATUS,
|
|
183
|
+
nextActionHour: 9,
|
|
184
|
+
}),
|
|
185
|
+
]);
|
|
186
|
+
await useCase.run(runParams);
|
|
187
|
+
expect(mockLocalCommandRunner.runCommand).toHaveBeenCalledWith('tmux', [
|
|
188
|
+
'kill-session',
|
|
189
|
+
'-t',
|
|
190
|
+
sessionName,
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('never kills an excluded-status session that has no reactivation trigger', async () => {
|
|
195
|
+
const issueUrl = 'https://github.com/user/repo/issues/14';
|
|
196
|
+
const sessionName = sessionNameOf(issueUrl);
|
|
197
|
+
setLiveSessions([`${sessionName} ${nowEpochSeconds}`]);
|
|
198
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([
|
|
199
|
+
createMockIssue({
|
|
200
|
+
url: issueUrl,
|
|
201
|
+
status: DEFAULT_EXCLUDED_STATUS,
|
|
202
|
+
nextActionDate: null,
|
|
203
|
+
nextActionHour: null,
|
|
204
|
+
}),
|
|
205
|
+
]);
|
|
206
|
+
await useCase.run(runParams);
|
|
207
|
+
expect(mockLocalCommandRunner.runCommand).not.toHaveBeenCalledWith('tmux', [
|
|
208
|
+
'kill-session',
|
|
209
|
+
'-t',
|
|
210
|
+
sessionName,
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('kills a no-task session that has been idle at least the idle threshold', async () => {
|
|
215
|
+
const sessionName = 'no_task_session';
|
|
216
|
+
const idleActivity = nowEpochSeconds - DEFAULT_IDLE_THRESHOLD_SECONDS;
|
|
217
|
+
setLiveSessions([`${sessionName} ${idleActivity}`]);
|
|
218
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([]);
|
|
219
|
+
await useCase.run(runParams);
|
|
220
|
+
expect(mockLocalCommandRunner.runCommand).toHaveBeenCalledWith('tmux', [
|
|
221
|
+
'kill-session',
|
|
222
|
+
'-t',
|
|
223
|
+
sessionName,
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('never kills a no-task session that was active within the idle threshold', async () => {
|
|
228
|
+
const sessionName = 'no_task_session';
|
|
229
|
+
const recentActivity = nowEpochSeconds - DEFAULT_IDLE_THRESHOLD_SECONDS + 1;
|
|
230
|
+
setLiveSessions([`${sessionName} ${recentActivity}`]);
|
|
231
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([]);
|
|
232
|
+
await useCase.run(runParams);
|
|
233
|
+
expect(mockLocalCommandRunner.runCommand).not.toHaveBeenCalledWith('tmux', [
|
|
234
|
+
'kill-session',
|
|
235
|
+
'-t',
|
|
236
|
+
sessionName,
|
|
237
|
+
]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('does nothing when there are no live sessions', async () => {
|
|
241
|
+
setLiveSessions(['', ' ']);
|
|
242
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([]);
|
|
243
|
+
await useCase.run(runParams);
|
|
244
|
+
expect(mockLocalCommandRunner.runCommand).toHaveBeenCalledTimes(1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('maps a session back to its issue using the dot-and-colon-to-underscore convention', async () => {
|
|
248
|
+
const issueUrl = 'https://github.com/owner/repo/issues/9';
|
|
249
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
250
|
+
expect(sessionNameOf(issueUrl)).toBe(sessionName);
|
|
251
|
+
setLiveSessions([`${sessionName} ${nowEpochSeconds}`]);
|
|
252
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([
|
|
253
|
+
createMockIssue({ url: issueUrl, status: 'In Progress' }),
|
|
254
|
+
]);
|
|
255
|
+
await useCase.run(runParams);
|
|
256
|
+
expect(mockLocalCommandRunner.runCommand).toHaveBeenCalledWith('tmux', [
|
|
257
|
+
'kill-session',
|
|
258
|
+
'-t',
|
|
259
|
+
sessionName,
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
2
|
+
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
|
+
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
4
|
+
import { Issue } from '../entities/Issue';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_EXCLUDED_STATUS = 'In Tmux by human';
|
|
7
|
+
export const DEFAULT_IDLE_THRESHOLD_SECONDS = 24 * 60 * 60;
|
|
8
|
+
|
|
9
|
+
type LiveTmuxSession = {
|
|
10
|
+
sessionName: string;
|
|
11
|
+
activityEpochSeconds: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type KillCandidate = {
|
|
15
|
+
sessionName: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class StaleTmuxSessionKillUseCase {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly projectRepository: Pick<ProjectRepository, 'getByUrl'>,
|
|
22
|
+
private readonly issueRepository: Pick<IssueRepository, 'getAllOpened'>,
|
|
23
|
+
private readonly localCommandRunner: Pick<LocalCommandRunner, 'runCommand'>,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
run = async (params: {
|
|
27
|
+
projectUrl: string;
|
|
28
|
+
excludedStatus: string;
|
|
29
|
+
idleThresholdSeconds: number;
|
|
30
|
+
now: Date;
|
|
31
|
+
}): Promise<void> => {
|
|
32
|
+
const liveSessions = await this.listLiveSessions();
|
|
33
|
+
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
34
|
+
const openIssues = await this.issueRepository.getAllOpened(project);
|
|
35
|
+
const issueBySessionName = new Map<string, Issue>();
|
|
36
|
+
for (const issue of openIssues) {
|
|
37
|
+
issueBySessionName.set(this.deriveSessionName(issue.url), issue);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nowEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
41
|
+
const killCandidates: KillCandidate[] = [];
|
|
42
|
+
for (const session of liveSessions) {
|
|
43
|
+
const reason = this.evaluateKillReason(
|
|
44
|
+
session,
|
|
45
|
+
issueBySessionName.get(session.sessionName) ?? null,
|
|
46
|
+
nowEpochSeconds,
|
|
47
|
+
params.excludedStatus,
|
|
48
|
+
params.idleThresholdSeconds,
|
|
49
|
+
);
|
|
50
|
+
if (reason !== null) {
|
|
51
|
+
killCandidates.push({ sessionName: session.sessionName, reason });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(
|
|
56
|
+
`Stale tmux session cleanup: ${killCandidates.length} kill candidate(s) of ${liveSessions.length} live session(s).`,
|
|
57
|
+
);
|
|
58
|
+
for (const candidate of killCandidates) {
|
|
59
|
+
console.log(
|
|
60
|
+
`Kill candidate: ${candidate.sessionName} (${candidate.reason})`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const candidate of killCandidates) {
|
|
65
|
+
await this.localCommandRunner.runCommand('tmux', [
|
|
66
|
+
'kill-session',
|
|
67
|
+
'-t',
|
|
68
|
+
candidate.sessionName,
|
|
69
|
+
]);
|
|
70
|
+
console.log(
|
|
71
|
+
`Killed tmux session: ${candidate.sessionName} (${candidate.reason})`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
private listLiveSessions = async (): Promise<LiveTmuxSession[]> => {
|
|
77
|
+
const result = await this.localCommandRunner.runCommand('tmux', [
|
|
78
|
+
'list-sessions',
|
|
79
|
+
'-F',
|
|
80
|
+
'#{session_name} #{session_activity}',
|
|
81
|
+
]);
|
|
82
|
+
return result.stdout
|
|
83
|
+
.split('\n')
|
|
84
|
+
.map((line) => line.trim())
|
|
85
|
+
.filter((line) => line.length > 0)
|
|
86
|
+
.map((line) => {
|
|
87
|
+
const separatorIndex = line.lastIndexOf(' ');
|
|
88
|
+
const sessionName = line.slice(0, separatorIndex);
|
|
89
|
+
const activityEpochSeconds = Number(line.slice(separatorIndex + 1));
|
|
90
|
+
return { sessionName, activityEpochSeconds };
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
private deriveSessionName = (issueUrl: string): string =>
|
|
95
|
+
issueUrl.replace(/[.:]/g, '_');
|
|
96
|
+
|
|
97
|
+
private evaluateKillReason = (
|
|
98
|
+
session: LiveTmuxSession,
|
|
99
|
+
issue: Issue | null,
|
|
100
|
+
nowEpochSeconds: number,
|
|
101
|
+
excludedStatus: string,
|
|
102
|
+
idleThresholdSeconds: number,
|
|
103
|
+
): string | null => {
|
|
104
|
+
if (issue !== null) {
|
|
105
|
+
if (issue.status !== excludedStatus) {
|
|
106
|
+
return `mapped to open issue ${issue.url} with status "${issue.status ?? 'null'}" which is not the excluded status "${excludedStatus}"`;
|
|
107
|
+
}
|
|
108
|
+
if (issue.nextActionDate !== null) {
|
|
109
|
+
return `mapped to open issue ${issue.url} which has a next action date set`;
|
|
110
|
+
}
|
|
111
|
+
if (issue.nextActionHour !== null) {
|
|
112
|
+
return `mapped to open issue ${issue.url} which has a next action hour set`;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const idleSeconds = nowEpochSeconds - session.activityEpochSeconds;
|
|
118
|
+
if (idleSeconds >= idleThresholdSeconds) {
|
|
119
|
+
return `maps to no open issue and has been idle for ${idleSeconds} seconds (threshold ${idleThresholdSeconds} seconds)`;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,KAAK,UAAU,GAAG;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,8BAA8B,CAAC,EAAE,MAAM,CAAC;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iCAAiC,CAAC,EAAE,MAAM,CAAC;CAC5C,CAAC;AA6CF,QAAA,MAAM,cAAc,GAAI,gBAAgB,MAAM,KAAG,UA6ChD,CAAC;AAEF,QAAA,MAAM,wBAAwB,GAAI,QAAQ,MAAM,KAAG,UAiDlD,CAAC;AAEF,QAAA,MAAM,YAAY,GAChB,YAAY,UAAU,EACtB,cAAc,UAAU,EACxB,iBAAiB,UAAU,KAC1B,UAmDD,CAAC;AAeH,QAAA,MAAM,OAAO,SAAgB,CAAC;AAyY9B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,wBAAwB,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
2
|
+
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
|
+
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
4
|
+
export declare const DEFAULT_EXCLUDED_STATUS = 'In Tmux by human';
|
|
5
|
+
export declare const DEFAULT_IDLE_THRESHOLD_SECONDS: number;
|
|
6
|
+
export declare class StaleTmuxSessionKillUseCase {
|
|
7
|
+
private readonly projectRepository;
|
|
8
|
+
private readonly issueRepository;
|
|
9
|
+
private readonly localCommandRunner;
|
|
10
|
+
constructor(
|
|
11
|
+
projectRepository: Pick<ProjectRepository, 'getByUrl'>,
|
|
12
|
+
issueRepository: Pick<IssueRepository, 'getAllOpened'>,
|
|
13
|
+
localCommandRunner: Pick<LocalCommandRunner, 'runCommand'>,
|
|
14
|
+
);
|
|
15
|
+
run: (params: {
|
|
16
|
+
projectUrl: string;
|
|
17
|
+
excludedStatus: string;
|
|
18
|
+
idleThresholdSeconds: number;
|
|
19
|
+
now: Date;
|
|
20
|
+
}) => Promise<void>;
|
|
21
|
+
private listLiveSessions;
|
|
22
|
+
private deriveSessionName;
|
|
23
|
+
private evaluateKillReason;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=StaleTmuxSessionKillUseCase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StaleTmuxSessionKillUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/StaleTmuxSessionKillUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAG7E,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAC1D,eAAO,MAAM,8BAA8B,QAAe,CAAC;AAY3D,qBAAa,2BAA2B;IAEpC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,kBAAkB;gBAFlB,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,EACtD,eAAe,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,EACtD,kBAAkB,EAAE,IAAI,CAAC,kBAAkB,EAAE,YAAY,CAAC;IAG7E,GAAG,GAAU,QAAQ;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,GAAG,EAAE,IAAI,CAAC;KACX,KAAG,OAAO,CAAC,IAAI,CAAC,CA2Cf;IAEF,OAAO,CAAC,gBAAgB,CAgBtB;IAEF,OAAO,CAAC,iBAAiB,CACQ;IAEjC,OAAO,CAAC,kBAAkB,CAyBxB;CACH"}
|