Skip to content

CodePipelineRouter ​

CodePipelineRouter routes a CodePipeline invoke action to a handler and reports the result back to the pipeline for you.

A pipeline runs your Lambda as a custom action and waits. The router works out which of your routes should handle the job, parses the action's user parameters, then calls PutJobSuccessResult or PutJobFailureResult on your behalf so the stage moves on.

Install ​

bash
npm install @lambda-event-router/base @lambda-event-router/codepipeline

@lambda-event-router/base is a peer dependency, so install it yourself. It exports LambdaRouter, which every router plugs into.

Create the router ​

ts
import { createCodePipelineRouter } from '@lambda-event-router/codepipeline'
import { logInvocation } from './middleware/logInvocation'

const codePipelineRouter = createCodePipelineRouter({
  middleware: [logInvocation],  // Optional
})

Both options can be left out. createCodePipelineRouter() on its own gives you a router that builds its own CodePipelineClient to report results with.

Options ​

OptionTypeRequiredDefaultDescription
clientCodePipelineClientNoA new CodePipelineClientThe SDK client the router calls PutJobSuccessResult and PutJobFailureResult on. Pass your own to set a region or credentials, or to share one across a Lambda
middlewareCodePipelineMiddleware[]No[]Runs for every job this router handles, before any route middleware. See Middleware

Register routes ​

ts
codePipelineRouter.route({
  filters: {
    functionName: 'deploy',
    hasInputArtifacts: true,
  },
  userParametersSchema: DeployParametersSchema,  // Optional
  middleware: [withDeployContext],  // Optional
  handler: runDeploy,
})

filters and handler are required, though filters can be an empty object to match every job.

route() returns the router, so you can chain registrations.

ts
codePipelineRouter.route(deployRoute).route(migrateRoute)

Routes are ranked by how specific they are. Where two overlap, the one matching only a subset of the other is tried first, whatever order you registered them in. Registration order decides the rest, so give each route filters no other route can match. See match order for how ranking works and what it cannot settle.

A job that matches no route reports failure to the pipeline and throws. The stage fails with No route matched for CodePipeline job <id> and the Lambda re-throws. Register a route with empty filters as a catch-all if you would rather handle unrecognised actions yourself, and see nothing matched for what the other routers do instead.

Convenience methods ​

continuation() sets the hasContinuationToken filter to true for you, so it only matches a job that CodePipeline has re-invoked with a token. You pass everything except that key. The two calls below register the same route.

ts
codePipelineRouter.continuation({
  filters: { functionName: 'deploy' },
  handler: resumeDeploy,
})

codePipelineRouter.route({
  filters: { functionName: 'deploy', hasContinuationToken: true },
  handler: resumeDeploy,
})
MethodSetsTakes
continuation()hasContinuationToken: trueThe filters without hasContinuationToken, plus userParametersSchema, middleware and handler

continuation() drops the hasContinuationToken key from the filters you pass and sets it itself, so putting it in the object is a type error. A long-running action returns a continuationToken from its handler and CodePipeline re-invokes the Lambda with the same token, which is the job this route catches. See convenience methods for how the other routers use them.

Filters ​

Every filter key on one route, showing each form a value can take. All of them are optional, so set the ones that pick out the jobs you want and leave the rest off.

ts
codePipelineRouter.route({
  filters: {
    functionName: ['deploy', 'migrate'], // Or a pattern: /^deploy-/
    hasInputArtifacts: true,
    hasContinuationToken: false,
    custom: ({ userParameters }) => isObject(userParameters) && userParameters.env === 'prod',
  },
  handler: runDeploy,
})
FilterTypeDescription
functionNameFilterStringMatcherMatches against the FunctionName set on the action configuration
hasInputArtifactsbooleantrue matches a job that has input artifacts, false a job that has none
hasContinuationTokenbooleantrue matches a re-invoked job carrying a continuation token, false a first invocation
custom(input: CodePipelineFilterInput) => boolean | Promise<boolean>Anything the other filters cannot express. Can be async

FilterStringMatcher is string | RegExp | Array<string | RegExp>. See filters for how each form matches, including the * wildcard.

custom is handed functionName, hasInputArtifacts, hasContinuationToken and the parsed userParameters. The user parameters have not been through your schema yet, so they arrive typed unknown. Narrow them with isObject from @lambda-event-router/base rather than reading straight in. Since the built-in keys already cover the function name and the artifact and token flags, a custom earns its place matching on userParameters, which nothing else reaches. See custom for where it sits in the filter order.

Handler ​

Handlers take one argument and return either a result or nothing.

ts
import { logger } from '@lambda-event-router/base'
import type { CodePipelineRequest, CodePipelineResponse } from '@lambda-event-router/codepipeline'

export async function runDeploy(request: CodePipelineRequest): Promise<CodePipelineResponse> {
  logger.info(`Running deploy job ${request.jobId}`)
  return { outputVariables: { status: 'deployed' } }
}

Request object ​

FieldTypeDescription
jobIdstringThe job ID, used when reporting the result back
functionNamestringThe FunctionName from the action configuration
userParametersTUserParametersThe UserParameters string, JSON parsed. Raw string if it is not JSON. Typed by your schema, otherwise unknown
inputArtifactsArtifact[]The input artifacts, each with a name and its S3 location
outputArtifactsArtifact[]The output artifacts the action is expected to produce
artifactCredentialsCredentialsTemporary credentials scoped to the artifact bucket
continuationTokenstring | undefinedThe token from a previous run, set only on a re-invocation
eventCodePipelineEventThe untouched event from AWS
contextContextThe Lambda context

Artifact, Credentials, CodePipelineEvent and Context come from aws-lambda, not from this package.

Response type ​

CodePipelineResponse is CodePipelineSuccessResult | void. Return nothing and the router reports the job succeeded. Return a result to hand values back to the pipeline.

ts
return {
  outputVariables: { deployedRegion: 'eu-west-2' },  // Available to later actions
  continuationToken: 'step-2',  // Re-invoke this action with the token
}

Both keys are optional. outputVariables become available to later actions in the pipeline, and a continuationToken tells CodePipeline to invoke the action again rather than treat it as done. See Responses for what the router does with the return value and how a throw is reported.

Inferred handlers ​

Attach a userParametersSchema and defineRoute types userParameters from it, so environment below is a string without you declaring that anywhere.

ts
import { logger } from '@lambda-event-router/base'
import { defineRoute } from '@lambda-event-router/codepipeline'
import { z } from 'zod'

const DeployParametersSchema = z.object({ environment: z.string(), region: z.string() })

export const deployRoute = defineRoute({
  filters: { functionName: 'deploy' },
  userParametersSchema: DeployParametersSchema,
}).handle(async ({ jobId, userParameters }) => {
  logger.info(`Deploying job ${jobId} to ${userParameters.environment}`)
  return { outputVariables: { region: userParameters.region } }
})

codePipelineRouter.route(deployRoute)

Not knowing the request shape pays off most in a Lambda taking several event sources, since you never have to look any of them up. See inferred handlers, where the same source is written both ways to compare.

Annotated handlers ​

Annotating the request yourself splits route setup from business logic, using CodePipelineRequest and the type of your parsed user parameters.

ts
// handlers/runDeploy.ts
import { logger } from '@lambda-event-router/base'
import type { CodePipelineRequest, CodePipelineResponse } from '@lambda-event-router/codepipeline'
import { z } from 'zod'

export const DeployParametersSchema = z.object({ environment: z.string(), region: z.string() })
type DeployParameters = z.infer<typeof DeployParametersSchema>

export async function runDeploy(
  request: CodePipelineRequest<DeployParameters>,
): Promise<CodePipelineResponse> {
  logger.info(`Deploying job ${request.jobId} to ${request.userParameters.environment}`)
  return { outputVariables: { region: request.userParameters.region } }
}
ts
// codepipeline.ts
import { createCodePipelineRouter } from '@lambda-event-router/codepipeline'
import { DeployParametersSchema, runDeploy } from './handlers/runDeploy'

const codePipelineRouter = createCodePipelineRouter()

codePipelineRouter.route({
  filters: { functionName: 'deploy' },
  userParametersSchema: DeployParametersSchema,
  handler: runDeploy,
})

Derive the type from the schema with z.infer rather than hand-writing an interface that mirrors it. See annotated handlers for the worked version.

Schema validation ​

userParametersSchema is the only schema key, and it is optional. It validates the parsed UserParameters after a route has matched.

ts
const DeployParametersSchema = z.object({
  environment: z.enum(['staging', 'production']),
  region: z.string(),
})

codePipelineRouter.route({
  filters: { functionName: 'deploy' },
  userParametersSchema: DeployParametersSchema,
  handler: runDeploy,
})
KeyValidates
userParametersSchemaThe UserParameters string after JSON parsing

Any Standard Schema library works. Validation runs after the route matches and before any middleware, so a job whose parameters fail their schema reports failure to the pipeline and throws, without the handler running. See schema validation for what your handler receives after coercion.

Responses ​

The router reports the outcome of every job back to CodePipeline, so the pipeline stage never hangs waiting on your Lambda.

When the handler returns, the router calls PutJobSuccessResult with the job ID. A returned outputVariables map and continuationToken are passed along, and returning nothing sends just the job ID.

When the handler throws, the router calls PutJobFailureResult with the error message as the failure detail, then re-throws so the invocation fails and Lambda records the error. A no-match and a schema failure are reported the same way, since both throw before the handler runs. If the report call itself fails the router swallows that error and still re-throws the original, so the real cause reaches your logs.

A continuationToken in the success result is how a long-running action tells CodePipeline it is not finished. The pipeline re-invokes the Lambda with the same token, and the continuation() route is where you pick that invocation back up.

Middleware ​

Router and route middleware are both typed CodePipelineMiddleware, and the chain runs once per job. Middleware wraps the handler, so it can read the result the handler returns and change what the router reports.

ts
import { logger } from '@lambda-event-router/base'
import type { CodePipelineMiddleware } from '@lambda-event-router/codepipeline'

export const logInvocation: CodePipelineMiddleware = async (request, next) => {
  logger.info(`Handling job ${request.jobId} for ${request.functionName}`)
  return next(request)
}
ts
const codePipelineRouter = createCodePipelineRouter({ middleware: [logInvocation] })

codePipelineRouter.route({
  filters: { functionName: 'deploy' },
  middleware: [withDeployContext],
  handler: runDeploy,
})

Route middleware carries the route's user parameters type. A route with a userParametersSchema needs CodePipelineMiddleware<DeployParameters>. CodePipelineMiddleware on its own does not compile there. Router middleware takes no type argument, because it runs for every route.

Middleware does not run when a schema fails, since validation comes first. See middleware for the execution order and the three levels it attaches at.

Types ​

All exported from @lambda-event-router/codepipeline.

TypeDescription
CodePipelineRequest<TUserParameters>The handler argument
CodePipelineResponseHandler return type, CodePipelineSuccessResult | void
CodePipelineSuccessResultThe success shape, { outputVariables?, continuationToken? }
CodePipelineHandler<TUserParameters>The handler function type
CodePipelineFiltersThe filters object
CodePipelineFilterInput<TUserParameters>What custom receives
CodePipelineRouteDefinition<TUserParameters>A full route passed to route()
CodePipelineMiddleware<TUserParameters>Router and route middleware
CodePipelineRouterOptionsOptions for createCodePipelineRouter

The CodePipelineRouter class and the createCodePipelineRouter and defineRoute functions come from the same place.

Generic parameters ​

The types above that take a parameter all take the same one.

ParameterTypesDefault
TUserParametersrequest.userParametersunknown

Leave it off and userParameters stays unknown, which is what you narrow in a handler with no schema. You only need it for annotated handlers; an inferred handler reads it from userParametersSchema.

Code example ​

One Lambda backing two custom actions, a deploy that hands output variables to later stages and a long-running migration that resumes itself through a continuation token.

Open a file: index.ts | CodePipeline router | handlers | schema

File viewer
TScodepipeline.ts

The migrate action has two routes. The continuation route matches only once CodePipeline re-invokes the job with a token, so the first invocation falls to startMigration and every resume falls to resumeMigration, whichever order they are registered in. The deploy action matches on a different function name, so it never collides with either.

index.ts hands the router to LambdaRouter, which is what AWS invokes and what every router in the Lambda gets registered on. See routers for how the two levels of matching fit together.