merge.generated.sh + user-owned hooksMerge 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>callsscripts/merge.generated.sh --dry-runand resolves the same file policies and hooks without modifying the project.make merge ARCHIVE=<archive>accepts one.zip,.tar.gzor.tgzarchive, 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
generatedbasename convention andscripts/merge-overwrite.txt. - If executable,
scripts/merge_file.shruns 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-staleflag. - If executable,
scripts/merge_post.shruns after the file and stale passes but before generated/business interface validation. - The incoming
merge_validate.generated.pythen 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.
ADDDestination missingCopy the incoming file and preserve its mode.
UPDGenerated basenameReplace an existing file whose basename contains generated.
OVROverwrite listReplace an existing exact project-relative path from scripts/merge-overwrite.txt.
SKPUser ownedPreserve an existing file not covered by generated basename or overwrite-list ownership.
MRGHook outputReplace an existing file with the custom result produced by merge_file.sh.
ADD_HOOKHook outputCreate a missing file from the custom result produced by merge_file.sh.
STALELocal generated fileThe 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.
$1Relative pathProject-relative incoming path, such as README.md or <service>/config/config.yaml.
$2Current fileAbsolute current destination path, or an empty string when the file does not exist.
$3Incoming fileAbsolute path to the unpacked generated file.
$4Custom outputWritable temporary path where exit code 12 must place the complete merged result.
$5Default actionOne of ADD, UPD, OVR or SKP.
$6Generated flag1 when the incoming basename contains generated; otherwise 0.
$7Overwrite flag1 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.
0DefaultKeep the action selected by ServiceGen.
10Preserve / skipUse SKP, even when the file would normally be generated-owned or overwrite-listed. For a missing destination, skip the incoming file.
11Accept incomingForce OVR for an existing destination or ADD for a missing destination.
12Use custom outputInstall 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 codeAbortStop 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.
SERVICEGEN_MERGE_DRY_RUN`0` or `1`1 during merge-check. Hooks must not mutate the project in this mode.
SERVICEGEN_PROJECT_DIRAbsolute pathRoot of the existing workspace being updated.
SERVICEGEN_INCOMING_ROOTAbsolute pathRoot 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.
$1Project rootAbsolute root of the updated local workspace.
$2Incoming rootAbsolute root of the still-available unpacked generated workspace.
Exit 0ContinueProceed to generated/business interface validation.
Non-zero exitAbort validationReport SG_MERGE_HOOK_FAILED and stop before interface validation. Already installed files are not rolled back.
Typical workProject workflowRun 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.
.servicegen/merge.logAlways rewrittenHuman-readable file decisions, hook messages, diagnostics and counters for the latest merge.
.servicegen/migrations/merge-YYYYMMDD-HHMMSS.mdOn required migrationChecklist created after a real merge when preserved business code is missing a newly required maker.
scripts/merge_validate.generated.pyGenerated validatorCan be rerun with make merge-validate; also supports --format json for the versioned diagnostic envelope.
scripts/merge-overwrite.txtGenerated ownership listExact 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