SAService Architect Back to designer
SAService ArchitectDocumentation
Code generation merge.generated.sh + user-owned hooks

Merge engine and hooks

The generated merge engine applies a complete ServiceGen archive to an existing workspace while preserving business files. Two user-owned executable hooks can customize individual file decisions and run project-wide work after installation.

When to use

Use this reference when regeneration needs custom three-way merging, protected local files, generated README handling, formatting, tests or project-specific migration automation.

Behavior

  • make merge-check ARCHIVE=<archive> calls scripts/merge.generated.sh --dry-run and resolves the same file policies and hooks without modifying the project.
  • make merge ARCHIVE=<archive> accepts one .zip, .tar.gz or .tgz archive, unpacks it to a temporary directory and locates the generated project root.
  • For every incoming file, the engine calculates a default action from destination existence, the generated basename convention and scripts/merge-overwrite.txt.
  • If executable, scripts/merge_file.sh runs once per incoming file and may keep the default, preserve the current file, force the incoming file or install a custom merged result.
  • All per-file hook decisions are resolved before the first project file is changed. A failing file hook therefore cannot leave a partially applied file pass.
  • After applying files, the engine reports stale generated files and optionally removes them with the explicit --remove-stale flag.
  • If executable, scripts/merge_post.sh runs after the file and stale passes but before generated/business interface validation.
  • The incoming merge_validate.generated.py then checks that preserved business files still provide makers required by newly generated application assembly.
  • Every decision, hook message, diagnostic and summary is written to .servicegen/merge.log.

Execution order

The ordering is part of the extension contract. There is no separate project-wide pre-merge hook; the per-file hook phase acts as an all-files preflight before mutation.

  • Validate arguments and archive format
  • Unpack the incoming project into a temporary directory
  • Read the incoming overwrite list
  • Calculate every default file action
  • Run every per-file hook and store its resolved decision
  • Apply ADD, UPD, OVR, MRG or SKP decisions
  • Detect and optionally remove stale generated files
  • Run the user-owned post-merge hook
  • Run generated/business interface validation
  • Write the merge report and activate a deferred merge-script self-update

Authoritative overwrite policy

When the incoming archive contains scripts/merge-overwrite.txt, that list is authoritative immediately. Added entries begin overwriting in the current merge and removed entries stop overwriting in the current merge. The installed list is used only for legacy archives without an incoming list.

Merge script self-update

When merge.generated.sh itself is updated, the engine stages the new copy and replaces the running script only after the merge report. This avoids overwriting the executable while its current process is still using it.

Default file actions

Actions calculated before a file hook overrides the decision.

ActionConditionDescription
ADDDestination missing

Copy the incoming file and preserve its mode.

UPDGenerated basename

Replace an existing file whose basename contains generated.

OVROverwrite list

Replace an existing exact project-relative path from scripts/merge-overwrite.txt.

SKPUser owned

Preserve an existing file not covered by generated basename or overwrite-list ownership.

MRGHook output

Replace an existing file with the custom result produced by merge_file.sh.

ADD_HOOKHook output

Create a missing file from the custom result produced by merge_file.sh.

STALELocal generated file

The local generated-basename file is absent from the incoming archive; report it and keep it unless removal was explicitly requested.

`scripts/merge_file.sh` arguments

The hook receives seven positional arguments for every incoming file.

ArgumentValueDescription
$1Relative path

Project-relative incoming path, such as README.md or <service>/config/config.yaml.

$2Current file

Absolute current destination path, or an empty string when the file does not exist.

$3Incoming file

Absolute path to the unpacked generated file.

$4Custom output

Writable temporary path where exit code 12 must place the complete merged result.

$5Default action

One of ADD, UPD, OVR or SKP.

$6Generated flag

1 when the incoming basename contains generated; otherwise 0.

$7Overwrite flag

1 when the exact path is present in the active overwrite list; otherwise 0.

Per-file hook exit codes

Exit status is the control API; printed text is logging only.

Exit codeResolved actionDescription
0Default

Keep the action selected by ServiceGen.

10Preserve / skip

Use SKP, even when the file would normally be generated-owned or overwrite-listed. For a missing destination, skip the incoming file.

11Accept incoming

Force OVR for an existing destination or ADD for a missing destination.

12Use custom output

Install the complete file written to $4 as MRG or ADD_HOOK. Returning 12 without creating $4 fails with SG_MERGE_HOOK_OUTPUT_MISSING.

Any other codeAbort

Stop with SG_MERGE_HOOK_FAILED. Because all file hooks run before mutation, the file pass has not started.

Hook environment

Variables available to both merge hooks.

VariableValueDescription
SERVICEGEN_MERGE_DRY_RUN`0` or `1`

1 during merge-check. Hooks must not mutate the project in this mode.

SERVICEGEN_PROJECT_DIRAbsolute path

Root of the existing workspace being updated.

SERVICEGEN_INCOMING_ROOTAbsolute path

Root of the unpacked generated workspace. Available until the merge process exits.

`scripts/merge_post.sh` contract

Project-wide hook executed after incoming files and stale processing.

ContractMeaningDescription
$1Project root

Absolute root of the updated local workspace.

$2Incoming root

Absolute root of the still-available unpacked generated workspace.

Exit 0Continue

Proceed to generated/business interface validation.

Non-zero exitAbort validation

Report SG_MERGE_HOOK_FAILED and stop before interface validation. Already installed files are not rolled back.

Typical workProject workflow

Run formatters, regenerate project-owned indexes, compare project files, execute focused tests or delegate to another user-owned script.

Reports and migration artifacts

Persistent output produced by merge and validation.

PathCreated whenDescription
.servicegen/merge.logAlways rewritten

Human-readable file decisions, hook messages, diagnostics and counters for the latest merge.

.servicegen/migrations/merge-YYYYMMDD-HHMMSS.mdOn required migration

Checklist created after a real merge when preserved business code is missing a newly required maker.

scripts/merge_validate.generated.pyGenerated validator

Can be rerun with make merge-validate; also supports --format json for the versioned diagnostic envelope.

scripts/merge-overwrite.txtGenerated ownership list

Exact public paths that the incoming generator snapshot owns even without generated in the basename.

Custom merge hooks

# scripts/merge_file.sh: merge .gitignore while preserving local order
if [[ "$1" == ".gitignore" ]]; then
  if [[ -z "$2" ]]; then exit 11; fi
  awk 'NR == FNR { seen[$0] = 1; print; next } !seen[$0]++ { print }' \
    "$2" "$3" > "$4"
  exit 12
fi
exit 0

# scripts/merge_post.sh: preview safely, then run project checks
if [[ "${SERVICEGEN_MERGE_DRY_RUN:-0}" == 1 ]]; then
  echo "would format and test the merged workspace"
  exit 0
fi
cd "$1"
make fmt
make test