Skip to content

Commit 3deee7c

Browse files
committed
bugfix-319-setup-crash-on-repository-with-no-issues: Improve configuration merging to preserve all undefined stored keys and prevent saving results, adding robustness tests.
1 parent b58fd19 commit 3deee7c

4 files changed

Lines changed: 105 additions & 40 deletions

File tree

build/cli/index.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52955,15 +52955,6 @@ exports.ConfigurationHandler = void 0;
5295552955
const config_1 = __nccwpck_require__(1106);
5295652956
const logger_1 = __nccwpck_require__(8836);
5295752957
const issue_content_interface_1 = __nccwpck_require__(9913);
52958-
/** Keys that must be preserved from stored config when current has undefined (e.g. when branch already existed). */
52959-
const CONFIG_KEYS_TO_PRESERVE = [
52960-
'parentBranch',
52961-
'workingBranch',
52962-
'releaseBranch',
52963-
'hotfixBranch',
52964-
'hotfixOriginBranch',
52965-
'branchType',
52966-
];
5296752958
class ConfigurationHandler extends issue_content_interface_1.IssueContentInterface {
5296852959
constructor() {
5296952960
super(...arguments);
@@ -52983,7 +52974,8 @@ class ConfigurationHandler extends issue_content_interface_1.IssueContentInterfa
5298352974
if (storedRaw != null && storedRaw.trim().length > 0) {
5298452975
try {
5298552976
const stored = JSON.parse(storedRaw);
52986-
for (const key of CONFIG_KEYS_TO_PRESERVE) {
52977+
// Merge all fields from stored that are undefined in current payload
52978+
for (const key in stored) {
5298752979
if (payload[key] === undefined && stored[key] !== undefined) {
5298852980
payload[key] = stored[key];
5298952981
}
@@ -52993,6 +52985,8 @@ class ConfigurationHandler extends issue_content_interface_1.IssueContentInterfa
5299352985
/* ignore parse errors, save current as-is */
5299452986
}
5299552987
}
52988+
// Ensure results is never saved to prevent payload bloat
52989+
delete payload['results'];
5299652990
return await this.internalUpdate(execution, JSON.stringify(payload, null, 4));
5299752991
}
5299852992
catch (error) {

build/github_action/index.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48036,15 +48036,6 @@ exports.ConfigurationHandler = void 0;
4803648036
const config_1 = __nccwpck_require__(1106);
4803748037
const logger_1 = __nccwpck_require__(8836);
4803848038
const issue_content_interface_1 = __nccwpck_require__(9913);
48039-
/** Keys that must be preserved from stored config when current has undefined (e.g. when branch already existed). */
48040-
const CONFIG_KEYS_TO_PRESERVE = [
48041-
'parentBranch',
48042-
'workingBranch',
48043-
'releaseBranch',
48044-
'hotfixBranch',
48045-
'hotfixOriginBranch',
48046-
'branchType',
48047-
];
4804848039
class ConfigurationHandler extends issue_content_interface_1.IssueContentInterface {
4804948040
constructor() {
4805048041
super(...arguments);
@@ -48064,7 +48055,8 @@ class ConfigurationHandler extends issue_content_interface_1.IssueContentInterfa
4806448055
if (storedRaw != null && storedRaw.trim().length > 0) {
4806548056
try {
4806648057
const stored = JSON.parse(storedRaw);
48067-
for (const key of CONFIG_KEYS_TO_PRESERVE) {
48058+
// Merge all fields from stored that are undefined in current payload
48059+
for (const key in stored) {
4806848060
if (payload[key] === undefined && stored[key] !== undefined) {
4806948061
payload[key] = stored[key];
4807048062
}
@@ -48074,6 +48066,8 @@ class ConfigurationHandler extends issue_content_interface_1.IssueContentInterfa
4807448066
/* ignore parse errors, save current as-is */
4807548067
}
4807648068
}
48069+
// Ensure results is never saved to prevent payload bloat
48070+
delete payload['results'];
4807748071
return await this.internalUpdate(execution, JSON.stringify(payload, null, 4));
4807848072
}
4807948073
catch (error) {

src/manager/description/__tests__/configuration_handler.test.ts

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,18 @@ describe('ConfigurationHandler', () => {
106106
expect(updatedDesc).toMatch(/"workingBranch":\s*"feature\/123"/);
107107
});
108108

109-
it('preserves stored keys when current has undefined', async () => {
109+
it('preserves all stored keys (including unknown ones) when current has undefined', async () => {
110110
const storedJson = JSON.stringify({
111111
parentBranch: 'main',
112-
releaseBranch: 'release/1',
113-
branchType: 'hotfix',
112+
unknownKey: 'preserve-me',
113+
branchConfiguration: { name: 'leaf' },
114114
});
115115
mockGetDescription.mockResolvedValue(descriptionWithConfig(storedJson));
116116
mockUpdateDescription.mockResolvedValue(undefined);
117117

118118
const execution = minimalExecution({
119119
currentConfiguration: {
120-
branchType: 'feature',
121-
releaseBranch: undefined,
122-
workingBranch: 'feature/123',
123120
parentBranch: undefined,
124-
hotfixOriginBranch: undefined,
125-
hotfixBranch: undefined,
126121
branchConfiguration: undefined,
127122
},
128123
});
@@ -131,8 +126,80 @@ describe('ConfigurationHandler', () => {
131126

132127
expect(mockUpdateDescription).toHaveBeenCalled();
133128
const fullDesc = mockUpdateDescription.mock.calls[0][3];
134-
expect(fullDesc).toContain('"parentBranch": "main"');
135-
expect(fullDesc).toContain('"releaseBranch": "release/1"');
129+
const parsed = JSON.parse(handler.getContent(fullDesc)!.trim());
130+
expect(parsed.parentBranch).toBe('main');
131+
expect(parsed.unknownKey).toBe('preserve-me');
132+
expect(parsed.branchConfiguration).toEqual({ name: 'leaf' });
133+
});
134+
135+
it('always excludes results from the saved payload even if present in stored', async () => {
136+
const storedJson = JSON.stringify({
137+
results: [{ some: 'result' }],
138+
parentBranch: 'main',
139+
});
140+
mockGetDescription.mockResolvedValue(descriptionWithConfig(storedJson));
141+
mockUpdateDescription.mockResolvedValue(undefined);
142+
143+
const execution = minimalExecution({
144+
currentConfiguration: {
145+
parentBranch: 'develop',
146+
},
147+
});
148+
149+
await handler.update(execution);
150+
151+
const fullDesc = mockUpdateDescription.mock.calls[0][3];
152+
const parsed = JSON.parse(handler.getContent(fullDesc)!.trim());
153+
expect(parsed.results).toBeUndefined();
154+
expect(parsed.parentBranch).toBe('develop');
155+
});
156+
157+
it('fails safely when block is mangled (missing end tag)', async () => {
158+
const mangledDesc = `body\n${CONFIG_START}\n{"x":1}\nno end tag here`;
159+
mockGetDescription.mockResolvedValue(mangledDesc);
160+
161+
const execution = minimalExecution();
162+
const result = await handler.update(execution);
163+
164+
// Should log error and return undefined instead of corrupting or crashing
165+
expect(result).toBeUndefined();
166+
const { logError } = require('../../../utils/logger');
167+
expect(logError).toHaveBeenCalledWith(expect.stringContaining('problem with open-close tags'));
168+
});
169+
170+
it('handles malformed JSON in stored config gracefully', async () => {
171+
mockGetDescription.mockResolvedValue(descriptionWithConfig('invalid { json'));
172+
mockUpdateDescription.mockResolvedValue(undefined);
173+
174+
const execution = minimalExecution({
175+
currentConfiguration: {
176+
branchType: 'feature',
177+
workingBranch: 'feat/new',
178+
},
179+
});
180+
181+
await handler.update(execution);
182+
183+
expect(mockUpdateDescription).toHaveBeenCalled();
184+
const fullDesc = mockUpdateDescription.mock.calls[0][3];
185+
expect(fullDesc).toContain('"branchType": "feature"');
186+
expect(fullDesc).toContain('"workingBranch": "feat/new"');
187+
});
188+
189+
it('handles empty stored config block gracefully', async () => {
190+
mockGetDescription.mockResolvedValue(descriptionWithConfig(' '));
191+
mockUpdateDescription.mockResolvedValue(undefined);
192+
193+
const execution = minimalExecution({
194+
currentConfiguration: {
195+
branchType: 'feature',
196+
},
197+
});
198+
199+
await handler.update(execution);
200+
201+
expect(mockUpdateDescription).toHaveBeenCalled();
202+
expect(mockUpdateDescription.mock.calls[0][3]).toContain('"branchType": "feature"');
136203
});
137204

138205
it('returns undefined on error', async () => {
@@ -144,4 +211,19 @@ describe('ConfigurationHandler', () => {
144211
expect(result).toBeUndefined();
145212
});
146213
});
214+
215+
describe('edge cases', () => {
216+
it('get returns undefined when internalGetter returns empty string', async () => {
217+
mockGetDescription.mockResolvedValue('');
218+
const execution = minimalExecution();
219+
const result = await handler.get(execution);
220+
expect(result).toBeUndefined();
221+
});
222+
223+
it('get throws informative error on invalid JSON', async () => {
224+
mockGetDescription.mockResolvedValue(descriptionWithConfig('{ "broken": '));
225+
const execution = minimalExecution();
226+
await expect(handler.get(execution)).rejects.toThrow(/Unexpected end of JSON input|SyntaxError/);
227+
});
228+
});
147229
});

src/manager/description/configuration_handler.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,6 @@ import { Execution } from "../../data/model/execution";
33
import { logError } from "../../utils/logger";
44
import { IssueContentInterface } from "./base/issue_content_interface";
55

6-
/** Keys that must be preserved from stored config when current has undefined (e.g. when branch already existed). */
7-
const CONFIG_KEYS_TO_PRESERVE = [
8-
'parentBranch',
9-
'workingBranch',
10-
'releaseBranch',
11-
'hotfixBranch',
12-
'hotfixOriginBranch',
13-
'branchType',
14-
] as const;
156

167
export class ConfigurationHandler extends IssueContentInterface {
178
get id(): string {
@@ -39,7 +30,8 @@ export class ConfigurationHandler extends IssueContentInterface {
3930
if (storedRaw != null && storedRaw.trim().length > 0) {
4031
try {
4132
const stored = JSON.parse(storedRaw) as Record<string, unknown>;
42-
for (const key of CONFIG_KEYS_TO_PRESERVE) {
33+
// Merge all fields from stored that are undefined in current payload
34+
for (const key in stored) {
4335
if (payload[key] === undefined && stored[key] !== undefined) {
4436
payload[key] = stored[key];
4537
}
@@ -49,6 +41,9 @@ export class ConfigurationHandler extends IssueContentInterface {
4941
}
5042
}
5143

44+
// Ensure results is never saved to prevent payload bloat
45+
delete payload['results'];
46+
5247
return await this.internalUpdate(execution, JSON.stringify(payload, null, 4));
5348
} catch (error) {
5449
logError(`Error updating issue description: ${error}`);

0 commit comments

Comments
 (0)