Hooks
A listener can watch a run and report on it, and that is enough for logging, reports, and notifications. It is not enough for the requests that arrive later: refuse this change unless a ticket number is set, add a tag every resource must carry, record who approved the commit before state is written.
Those need to sit in the call path rather than beside it. A hook is a callback Octo runs around one of its steps, and because it is in the path it can change what is passed along, and it can throw.
The six points
| Hook | Runs |
|---|---|
preModelActionHooks | Before a model action's handle() |
postModelActionHooks | After it |
preResourceActionHooks | Before a resource action's handle() |
postResourceActionHooks | After it |
preCommitHooks | Before state is written at commit |
postCommitHooks | After it |
Model action hooks have the most reach, because their return value becomes the actionOutputs the
next step sees. Commit hooks bracket the moment Octo's state
changes. Resource action hooks observe, and only fire for external
resources, which are the only resources with
actions.
Registering from the definition file
hooks:
- importPath: "./dist/index.js"
method: registerShopHooks
The named export is called at boot, and returns the hooks to register:
import {
AddShopQueueServiceModelAction
} from './modules/service/shop-queue-service/models/service/actions/add-shop-queue-service.model.action.js';
export function registerShopHooks () {
return {
postModelActionHooks: [
{
action: new AddShopQueueServiceModelAction(),
handle: async (diff, actionInputs, actionOutputs) => {
for (const resource of Object.values(actionOutputs)) {
resource.tags['CostCentre'] = '4815';
}
return actionOutputs;
},
},
],
preCommitHooks: [
{
handle: async (app) => {
console.error(`committing ${app.name}`);
},
},
],
};
}
A model or resource action hook names the action it wraps; a commit hook does not, because there is only one commit.
Registering from a module
A module can bring its own, for the duration of that module:
override registerHooks() {
return {
postModelActionHooks: [
{ action: new AddShopQueueServiceModelAction(), handle: /* ... */ },
],
};
}
Use this when the behaviour belongs to the module rather than to the project: something your module's actions always need, not something one team's policy requires. Policy belongs in the definition file, where the team that owns the policy can see it.
What a model action hook receives
The same three arguments the action gets, and it must return actionOutputs:
handle: async (diff, actionInputs, actionOutputs) => {
// diff the change being handled
// actionInputs the module's inputs, metadata, and nodes
// actionOutputs the resources declared so far, yours to add to or change
return actionOutputs;
}
Pre hooks run in registration order, then the action, then post hooks, and each one's return value
is passed to the next. Forgetting to return actionOutputs from a hook drops every resource
declared so far.
Throwing from a hook stops the run, which is how a gate is built. Nothing has been created at the point a model action hook runs, so a refusal there costs nothing.
Hooks against listeners
They look similar and are for different jobs.
Use a listener to observe. It cannot change anything, it cannot stop a run, and it cannot break anything by being wrong. Reporting, logging, notifications.
Use a hook to participate. It changes what the next step receives, and it can refuse. Enforcement, policy, injection.
If what you are building only reads, make it a listener. A hook that only reads is a listener with the ability to break an apply by throwing unexpectedly.