@lumpcode/core 0.1.0 → 0.1.1

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 CHANGED
@@ -422,7 +422,7 @@ const steps: Steps = [
422
422
 
423
423
  ## Context Tracking with Git Commits
424
424
 
425
- Lumpcode uses normalized git commit messages to record which contexts have already been processed. Each completed context contributes **exactly one commit** whose subject equals `gitCommitMessageFn({ context, ... })`. On every subsequent call to `runLump`, contexts whose normalized commit message is already present on a remote branch are skipped so the run only works on what remains. This lets you call `runLump` repeatedly (e.g. in a cron job) and each invocation picks up where the previous one left off, progressively working through the full context list without ever re-processing a context.
425
+ Lumpcode uses normalized git commit messages to record which contexts have already been processed. Each completed context contributes **exactly one commit** whose message contains the string returned by `gitCommitMessageFn({ context, ... })` (written as the subject; still matched if squash moves it into the body). On every subsequent call to `runLump`, contexts whose marker is already present on a remote branch are skipped so the run only works on what remains. This lets you call `runLump` repeatedly (e.g. in a cron job) and each invocation picks up where the previous one left off, progressively working through the full context list without ever re-processing a context.
426
426
 
427
427
  > If you'd rather not wire up your own scheduler, [@lumpcode/cli](https://www.npmjs.com/package/@lumpcode/cli) ships a built-in daemon (`lumpcode start`) that calls `runLump` on a tick, with concurrent-branch limits and per-lump enable/disable already wired in.
428
428
 
@@ -434,7 +434,7 @@ By default, a context named `my_component_ts` produces a commit with the subject
434
434
  LUMP:my_component_ts
435
435
  ```
436
436
 
437
- You can customize this via the `[gitCommitMessageFn](#gitcommitmessagefn)` parameter. The returned subject **must be unique per context** -- it is the only thing tying the commit back to its context.
437
+ You can customize this via the `[gitCommitMessageFn](#gitcommitmessagefn)` parameter. The returned string **must be unique per context** -- it is the only thing tying the commit back to its context. Status matches that string anywhere in the full commit message, as long as it is not a prefix of a longer `[A-Za-z0-9_-]*` token (`foo` does not match `foo-bar`).
438
438
 
439
439
  ### Context statuses
440
440
 
@@ -458,7 +458,7 @@ A context with `dependsOnContexts` is only eligible when **all** of its listed d
458
458
 
459
459
  ```bash
460
460
  git fetch --all
461
- git log --remotes=origin --grep="^LUMP:" --format='%H %s'
461
+ git log --remotes=origin -F --grep="LUMP:" --format='%H %s'
462
462
  ```
463
463
 
464
464
  **Check if a specific context has been processed (any remote ref):**
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../src/helpers/executeStepsForContextList/main.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,WAAW,EACX,eAAe,EACf,uBAAuB,EACvB,OAAO,EACP,MAAM,EACN,aAAa,EAMb,aAAa,EAChB,MAAM,aAAa,CAAC;AASrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA4BjD,MAAM,MAAM,gCAAgC,CACxC,CAAC,SAAS,aAAa,GAAG,aAAa,EACvC,EAAE,SAAS,aAAa,GAAG,aAAa,IACxC,QAAQ,CAAC,IAAI,CACb,YAAY,CAAC,CAAC,EAAE,EAAE,CAAC,EACjB,YAAY,GACZ,UAAU,GACV,eAAe,GACf,OAAO,GACP,SAAS,GACT,YAAY,GACZ,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,GAClB,oBAAoB,GACpB,aAAa,GACb,kBAAkB,GAClB,qBAAqB,GACrB,0BAA0B,CAC/B,CAAC,GAAG;IACD,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qGAAqG;IACrG,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB,CAAA;AAED,wBAAsB,0BAA0B,CAC5C,CAAC,SAAS,aAAa,GAAG,aAAa,EACvC,EAAE,SAAS,aAAa,GAAG,aAAa,EAC1C,EACE,UAAU,EACV,QAAQ,EACR,aAAa,EACb,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,WAAW,EACX,KAAK,EACL,OAAO,EACP,gBAAgB,EAChB,UAAU,EACV,mBAAmB,EACnB,wBAAwB,EACxB,MAAM,EAAE,WAAW,EACnB,MAAM,GACT,EAAE,gCAAgC,CAAC,CAAC,EAAE,EAAE,CAAC;;;;;;;;;;;IAuUzC;AAED,MAAM,MAAM,gCAAgC,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../src/helpers/executeStepsForContextList/main.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,WAAW,EACX,eAAe,EACf,uBAAuB,EACvB,OAAO,EACP,MAAM,EACN,aAAa,EAMb,aAAa,EAChB,MAAM,aAAa,CAAC;AASrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA0FjD,MAAM,MAAM,gCAAgC,CACxC,CAAC,SAAS,aAAa,GAAG,aAAa,EACvC,EAAE,SAAS,aAAa,GAAG,aAAa,IACxC,QAAQ,CAAC,IAAI,CACb,YAAY,CAAC,CAAC,EAAE,EAAE,CAAC,EACjB,YAAY,GACZ,UAAU,GACV,eAAe,GACf,OAAO,GACP,SAAS,GACT,YAAY,GACZ,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,GAClB,oBAAoB,GACpB,aAAa,GACb,kBAAkB,GAClB,qBAAqB,GACrB,0BAA0B,CAC/B,CAAC,GAAG;IACD,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qGAAqG;IACrG,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB,CAAA;AAED,wBAAsB,0BAA0B,CAC5C,CAAC,SAAS,aAAa,GAAG,aAAa,EACvC,EAAE,SAAS,aAAa,GAAG,aAAa,EAC1C,EACE,UAAU,EACV,QAAQ,EACR,aAAa,EACb,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,WAAW,EACX,KAAK,EACL,OAAO,EACP,gBAAgB,EAChB,UAAU,EACV,mBAAmB,EACnB,wBAAwB,EACxB,MAAM,EAAE,WAAW,EACnB,MAAM,GACT,EAAE,gCAAgC,CAAC,CAAC,EAAE,EAAE,CAAC;;;;;;;;;;;IAkWzC;AAED,MAAM,MAAM,gCAAgC,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../src/helpers/getContextStatus/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAKpE,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,EAAE,MAAM,EAAE;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,aAAa,CAAC,CAwEzB"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../src/helpers/getContextStatus/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAUpE,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,EAAE,MAAM,EAAE;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,aAAa,CAAC,CAwEzB"}
package/dist/index.cjs CHANGED
@@ -3424,6 +3424,40 @@ function parseGitLogHashSubjectLines(stdout) {
3424
3424
  });
3425
3425
  }
3426
3426
 
3427
+ const GIT_LOG_HASH_BODY_FORMAT = '%x1e%H%x00%B';
3428
+ const RECORD_SEPARATOR = '\x1e';
3429
+ const HASH_MESSAGE_SEPARATOR = '\x00';
3430
+ /** Parses `git log --format='%x1e%H%x00%B'` stdout into commit hash and full-message pairs. */
3431
+ function parseGitLogHashBodyRecords(stdout) {
3432
+ const records = [];
3433
+ for (const record of stdout.split(RECORD_SEPARATOR)) {
3434
+ if (!record)
3435
+ continue;
3436
+ const nul = record.indexOf(HASH_MESSAGE_SEPARATOR);
3437
+ if (nul === -1)
3438
+ continue;
3439
+ const hash = record.slice(0, nul).trim();
3440
+ if (!hash)
3441
+ continue;
3442
+ records.push({
3443
+ hash,
3444
+ message: record.slice(nul + 1),
3445
+ });
3446
+ }
3447
+ return records;
3448
+ }
3449
+
3450
+ const MARKER_CONTINUE = '[A-Za-z0-9_-]';
3451
+ function escapeRegExp(value) {
3452
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3453
+ }
3454
+ /** True when `marker` occurs in `message` and is not a prefix of a longer `[A-Za-z0-9_-]*` token. */
3455
+ function commitMessageIncludesMarker(message, marker) {
3456
+ if (marker.length === 0)
3457
+ return false;
3458
+ return new RegExp(`${escapeRegExp(marker)}(?!${MARKER_CONTINUE})`).test(message);
3459
+ }
3460
+
3427
3461
  /** Node-style `code` from a caught unknown error (e.g. `ENOENT`), or undefined. */
3428
3462
  function nodeErrnoCode(error) {
3429
3463
  if (error && typeof error === 'object' && 'code' in error) {
@@ -3766,6 +3800,54 @@ async function collectStepsForContext(params) {
3766
3800
  return collectedSteps;
3767
3801
  }
3768
3802
 
3803
+ const ABORT_STEP_WALK_MESSAGE = 'Process aborted';
3804
+ function stepWalkAbortedFailure() {
3805
+ return {
3806
+ success: false,
3807
+ data: {
3808
+ message: ABORT_STEP_WALK_MESSAGE,
3809
+ reason: 'stepWalkFailed',
3810
+ },
3811
+ };
3812
+ }
3813
+ function createStepWalkAbortError() {
3814
+ const error = new Error(ABORT_STEP_WALK_MESSAGE);
3815
+ error.name = 'AbortError';
3816
+ return error;
3817
+ }
3818
+ function isStepWalkAbortError(error) {
3819
+ return (typeof error === 'object'
3820
+ && error !== null
3821
+ && 'name' in error
3822
+ && error.name === 'AbortError');
3823
+ }
3824
+ /**
3825
+ * Await user-hook work, but stop waiting once `signal` aborts so the walk can
3826
+ * unwind (locks/teardown). Does not cancel sync busy-loops on the event loop.
3827
+ */
3828
+ async function awaitUnlessAborted(work, signal) {
3829
+ if (!signal) {
3830
+ return await work;
3831
+ }
3832
+ signal.throwIfAborted();
3833
+ return new Promise((resolve, reject) => {
3834
+ const onAbort = () => {
3835
+ cleanup();
3836
+ reject(createStepWalkAbortError());
3837
+ };
3838
+ const cleanup = () => {
3839
+ signal.removeEventListener('abort', onAbort);
3840
+ };
3841
+ signal.addEventListener('abort', onAbort, { once: true });
3842
+ Promise.resolve(work).then((value) => {
3843
+ cleanup();
3844
+ resolve(value);
3845
+ }, (error) => {
3846
+ cleanup();
3847
+ reject(error);
3848
+ });
3849
+ });
3850
+ }
3769
3851
  async function runOptionalGitCommand(input) {
3770
3852
  const { label, getCommand, cwd, logger } = input;
3771
3853
  try {
@@ -3825,6 +3907,10 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3825
3907
  let runFailure;
3826
3908
  try {
3827
3909
  for (let i = 0; i < contextList.length; i++) {
3910
+ if (signal?.aborted) {
3911
+ runFailure = stepWalkAbortedFailure();
3912
+ break;
3913
+ }
3828
3914
  const context = contextList[i];
3829
3915
  logger.info(contextList.length > 1
3830
3916
  ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
@@ -3837,127 +3923,137 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3837
3923
  const contextRunState = setupResult?.contextRunState || {};
3838
3924
  let stepWalkFailure;
3839
3925
  async function walkAndExecuteSteps(stepsToExec, currStepIndex) {
3840
- for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3841
- if (stepWalkFailure) {
3842
- return;
3843
- }
3844
- const step = stepsToExec[stepIndex];
3845
- const nextCallHeadIndex = [...currStepIndex, stepIndex];
3846
- const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3847
- if (typeof step === 'function' || Array.isArray(step)) {
3848
- let subSteps = [];
3849
- if (typeof step === 'function') {
3850
- subSteps = await step({
3926
+ try {
3927
+ for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3928
+ if (stepWalkFailure) {
3929
+ return;
3930
+ }
3931
+ signal?.throwIfAborted();
3932
+ const step = stepsToExec[stepIndex];
3933
+ const nextCallHeadIndex = [...currStepIndex, stepIndex];
3934
+ const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3935
+ if (typeof step === 'function' || Array.isArray(step)) {
3936
+ const subSteps = typeof step === 'function'
3937
+ ? await awaitUnlessAborted(step({
3938
+ context,
3939
+ stepIndex: compositeStepIndex,
3940
+ contextRunState,
3941
+ lumpVariables,
3942
+ }), signal)
3943
+ : step;
3944
+ await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3945
+ continue;
3946
+ }
3947
+ logger.verbose(`step ${JSON.stringify(step)}`);
3948
+ const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3949
+ const prompt = promptFn
3950
+ ? await awaitUnlessAborted(promptFn({
3851
3951
  context,
3852
3952
  stepIndex: compositeStepIndex,
3853
3953
  contextRunState,
3854
3954
  lumpVariables,
3955
+ stepVariables,
3956
+ }), signal)
3957
+ : '';
3958
+ const command = await awaitUnlessAborted(commandFn({
3959
+ context,
3960
+ prompt,
3961
+ stepIndex: compositeStepIndex,
3962
+ contextRunState,
3963
+ lumpVariables,
3964
+ stepVariables,
3965
+ projectRoot,
3966
+ workspacePath,
3967
+ }), signal);
3968
+ let commandResult = '';
3969
+ let commandSucceeded = true;
3970
+ if (command != null) {
3971
+ const { executable, args, env } = command;
3972
+ logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3973
+ if (env != null) {
3974
+ logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3975
+ }
3976
+ logger.verbose(`workspacePath ${workspacePath}`);
3977
+ const commandExec = await execBinary({
3978
+ binaryPath: executable,
3979
+ args,
3980
+ timeoutMillis,
3981
+ stdio: ['inherit', 'pipe', 'pipe'],
3982
+ cwd: workspacePath,
3983
+ signal,
3984
+ ...(env != null ? { env: { ...process.env, ...env } } : {}),
3855
3985
  });
3986
+ logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3987
+ if (!commandExec.success) {
3988
+ const aborted = commandExec.data.reason === 'aborted';
3989
+ if (aborted || !continueOnError) {
3990
+ stepWalkFailure = {
3991
+ success: false,
3992
+ data: {
3993
+ message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3994
+ reason: 'stepWalkFailed',
3995
+ },
3996
+ };
3997
+ return;
3998
+ }
3999
+ commandSucceeded = false;
4000
+ commandResult = (commandExec.data.stdout
4001
+ || commandExec.data.stderr
4002
+ || commandExec.data.message
4003
+ || '').toString();
4004
+ logger.verbose(`commandResult ${commandResult}`);
4005
+ }
4006
+ else {
4007
+ commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
4008
+ logger.verbose(`commandResult ${commandResult}`);
4009
+ }
4010
+ if (commandSucceeded) {
4011
+ const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
4012
+ logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
4013
+ }
3856
4014
  }
3857
- else {
3858
- subSteps = step;
3859
- }
3860
- await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3861
- continue;
3862
- }
3863
- logger.verbose(`step ${JSON.stringify(step)}`);
3864
- const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3865
- const prompt = promptFn
3866
- ? await promptFn({
4015
+ const historyEntry = {
4016
+ commandResult,
4017
+ commandSucceeded,
3867
4018
  context,
4019
+ prompt,
3868
4020
  stepIndex: compositeStepIndex,
3869
4021
  contextRunState,
3870
4022
  lumpVariables,
3871
4023
  stepVariables,
3872
- })
3873
- : '';
3874
- const command = await commandFn({
3875
- context,
3876
- prompt,
3877
- stepIndex: compositeStepIndex,
3878
- contextRunState,
3879
- lumpVariables,
3880
- stepVariables,
3881
- projectRoot,
3882
- workspacePath,
3883
- });
3884
- let commandResult = '';
3885
- let commandSucceeded = true;
3886
- if (command != null) {
3887
- const { executable, args, env } = command;
3888
- logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3889
- if (env != null) {
3890
- logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3891
- }
3892
- logger.verbose(`workspacePath ${workspacePath}`);
3893
- const commandExec = await execBinary({
3894
- binaryPath: executable,
3895
- args,
3896
- timeoutMillis,
3897
- stdio: ['inherit', 'pipe', 'pipe'],
3898
- cwd: workspacePath,
4024
+ projectRoot,
4025
+ };
4026
+ const postCommandExecFnInput = {
4027
+ ...historyEntry,
3899
4028
  signal,
3900
- ...(env != null ? { env: { ...process.env, ...env } } : {}),
3901
- });
3902
- logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3903
- if (!commandExec.success) {
3904
- const aborted = commandExec.data.reason === 'aborted';
3905
- if (aborted || !continueOnError) {
3906
- stepWalkFailure = {
3907
- success: false,
3908
- data: {
3909
- message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3910
- reason: 'stepWalkFailed',
3911
- },
3912
- };
3913
- return;
4029
+ };
4030
+ logger.verbose(`context is ${JSON.stringify(context)}`);
4031
+ const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
4032
+ logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
4033
+ if (!!command && keepHistoryFilePath.length > 0) {
4034
+ const appendResult = await appendHistoryEntry({
4035
+ filePath: keepHistoryFilePath,
4036
+ entry: historyEntry,
4037
+ });
4038
+ if (!appendResult.success) {
4039
+ // TODO: sanitize history appending to avoid this warning for certain commands outputs
4040
+ logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3914
4041
  }
3915
- commandSucceeded = false;
3916
- commandResult = (commandExec.data.stdout
3917
- || commandExec.data.stderr
3918
- || commandExec.data.message
3919
- || '').toString();
3920
- logger.verbose(`commandResult ${commandResult}`);
3921
- }
3922
- else {
3923
- commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3924
- logger.verbose(`commandResult ${commandResult}`);
3925
- }
3926
- if (commandSucceeded) {
3927
- const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3928
- logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3929
4042
  }
3930
- }
3931
- const postCommandExecFnInput = {
3932
- commandResult,
3933
- commandSucceeded,
3934
- context,
3935
- prompt,
3936
- stepIndex: compositeStepIndex,
3937
- contextRunState,
3938
- lumpVariables,
3939
- stepVariables,
3940
- projectRoot,
3941
- };
3942
- logger.verbose(`context is ${JSON.stringify(context)}`);
3943
- const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
3944
- logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
3945
- if (!!command && keepHistoryFilePath.length > 0) {
3946
- const appendResult = await appendHistoryEntry({
3947
- filePath: keepHistoryFilePath,
3948
- entry: postCommandExecFnInput,
3949
- });
3950
- if (!appendResult.success) {
3951
- // TODO: sanitize history appending to avoid this warning for certain commands outputs
3952
- logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
4043
+ if (postCommandExecFn) {
4044
+ const returnedSteps = await awaitUnlessAborted(postCommandExecFn(postCommandExecFnInput), signal);
4045
+ if (returnedSteps != null && returnedSteps.length > 0) {
4046
+ await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
4047
+ }
3953
4048
  }
3954
4049
  }
3955
- if (postCommandExecFn) {
3956
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3957
- if (returnedSteps != null && returnedSteps.length > 0) {
3958
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3959
- }
4050
+ }
4051
+ catch (error) {
4052
+ if (isStepWalkAbortError(error)) {
4053
+ stepWalkFailure = stepWalkAbortedFailure();
4054
+ return;
3960
4055
  }
4056
+ throw error;
3961
4057
  }
3962
4058
  }
3963
4059
  try {
@@ -4159,13 +4255,13 @@ async function getContextStatus(params) {
4159
4255
  if (!fetchResult.success)
4160
4256
  return 'toDo';
4161
4257
  }
4162
- const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote('%H %s')}`, { cwd: projectRoot });
4258
+ const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote(GIT_LOG_HASH_BODY_FORMAT)}`, { cwd: projectRoot });
4163
4259
  if (!logResult.success)
4164
4260
  return 'toDo';
4165
4261
  logger?.verbose(`logResult ${JSON.stringify(logResult.data)}`);
4166
4262
  const logResultOutput = logResult.data.stdout || logResult.data.stderr || '';
4167
- const matchingHashes = parseGitLogHashSubjectLines(logResultOutput)
4168
- .filter((entry) => entry.subject === commitMessage)
4263
+ const matchingHashes = parseGitLogHashBodyRecords(logResultOutput)
4264
+ .filter((entry) => commitMessageIncludesMarker(entry.message, commitMessage))
4169
4265
  .map((entry) => entry.hash);
4170
4266
  logger?.verbose(`contextName ${contextName}`);
4171
4267
  logger?.verbose(`remoteName ${remoteName}`);
@@ -4384,8 +4480,10 @@ async function runLump(input) {
4384
4480
  });
4385
4481
  }
4386
4482
 
4483
+ exports.GIT_LOG_HASH_BODY_FORMAT = GIT_LOG_HASH_BODY_FORMAT;
4387
4484
  exports.appendHistoryEntry = appendHistoryEntry;
4388
4485
  exports.collectStepsForContext = collectStepsForContext;
4486
+ exports.commitMessageIncludesMarker = commitMessageIncludesMarker;
4389
4487
  exports.contextStatus = contextStatus;
4390
4488
  exports.createConsoleLogger = createConsoleLogger;
4391
4489
  exports.defaultGitAddCommandFn = defaultGitAddCommandFn;
@@ -4408,6 +4506,7 @@ exports.historyFormatFromPath = historyFormatFromPath;
4408
4506
  exports.isProcessAlive = isProcessAlive;
4409
4507
  exports.killProcessTree = killProcessTree;
4410
4508
  exports.nodeErrnoCode = nodeErrnoCode;
4509
+ exports.parseGitLogHashBodyRecords = parseGitLogHashBodyRecords;
4411
4510
  exports.parseGitLogHashSubjectLines = parseGitLogHashSubjectLines;
4412
4511
  exports.pathExists = pathExists;
4413
4512
  exports.readHistoryFile = readHistoryFile;