Base template to deploy your next source function.
- Click
Use This Templateabove.- (If Segment PS, add to the
Segment Services EngineeringOrganization.)
- (If Segment PS, add to the
nvm use(to get the right version of NodeJS)npm install(to install npm dependencies)
npm run test
- The Buildkite pipeline also runs tests before every deploy.
- Tests live in
src/index.test.js; the function code lives insrc/index.js.
Deploying a source function is a two-call operation against the Segment
Public API, and it touches three different ids. Getting these straight is
the single most important thing in this repo — mixing them up produces
confusing 400 / "provide a valid Source function instance ID" errors.
| Id | Prefix / shape | What it is | Where you find it |
|---|---|---|---|
| Function id | sfnc_... |
The source-function definition (the code). | Segment UI URL of the function, or GET /functions. |
| Source id | 22-char, e.g. aj3EN6RALrMxFZe7q3Cjd8 |
The connected source that wraps the function. | Source → Settings → API Keys tab ("Source ID"). |
| Source function instance id | sfn_ + the Source id |
The deploy target. | Derived: take the Source id and prefix it with sfn_. |
⚠️ The gotcha: the deploy endpoint (POST /functions/{id}/deploy) does not accept thesfnc_function id, and it does not accept the raw Source id either. It requires thesfn_-prefixed "source function instance" id — i.e. the Source id from the API Keys tab withsfn_prepended. Example: Source idaj3EN6RALrMxFZe7q3Cjd8→ deploy idsfn_aj3EN6RALrMxFZe7q3Cjd8.
scripts/deploySourceFunction.js does two API calls per run:
- Push code —
PATCH https://api.segmentapis.com/functions/{FUNCTION_ID}with the contents ofsrc/index.js. Uses thesfnc_id. - Deploy to the connected source —
POST https://api.segmentapis.com/functions/{SOURCE_ID}/deploy. Uses thesfn_id (Source id prefixed withsfn_).
So the two env vars are:
FUNCTION_ID= thesfnc_...function id.SOURCE_ID=sfn_+ the Source id (NOT the raw Source id, NOT thesfnc_id).
(A destination function, by contrast, only PATCHes the function — it has no second "deploy to source" call. That's the core source-vs-destination difference.)
Each environment (DEV / QA / PROD) needs its own function and its own connected source. To create a new environment's pair via the Public API:
POST /functionswith{ "displayName": "<ENV> - Source Function Template", "resourceType": "SOURCE", "code": "<contents of src/index.js>" }. The response includes thesfnc_...id and acatalogId(which is just the id minus thesfnc_prefix).POST /sourceswith{ "slug": "<env>-source-function-template", "enabled": true, "metadataId": "<catalogId from step 1>" }. The response includes the 22-char Source id.- The deploy id for that env is then
sfn_<Source id>.
Tip: the Segment Public API returns the function
codefield with literal unescaped newlines, so piping the create response throughjqfails. Extract ids withgrep -oE '"id":"sfnc_[A-Za-z0-9]+"'instead — the resource is still created successfully.
All three environments live in the same Segment workspace; naming them
DEV - / QA - / PROD - Source Function Template keeps them
distinguishable.
This is the out-of-the-box deploy path — if you used this template, this is
what works with no extra infrastructure. The workflow is defined in
.github/workflows/deploySourceFunction.yml;
it installs dependencies, runs tests, then deploys via scripts/deploySourceFunction.js.
- Create GitHub Environments in
Settings→Environments→DEV(repeat forQA&PROD).DEVis enabled by default in the workflow; uncomment theQA/PRODjobs when you're ready to promote to them. - Create the function + connected source per environment in your Segment workspace (see "One function + one source per environment" above).
- Create a Public API Token to allow deploying.
- Add these Environment Secrets to each environment:
FUNCTION_ID— thesfnc_...function id.SOURCE_ID— thesfn_-prefixed source function instance id (the Source id from Settings → API Keys, withsfn_prepended).PUBLIC_API_TOKEN— get an API token.
To promote: add the !!_RELEASE_TO_QA label to the PR to deploy to QA; merge to
main to deploy to PROD.
Inside Twilio we deploy through Buildkite instead of GitHub-hosted runners. The
pipeline is defined in .buildkite/pipeline.yml;
each environment step installs dependencies, runs tests, then deploys via the
same scripts/deploySourceFunction.js.
- Create a Buildkite pipeline pointed at this repo (GitHub webhook + a queue
with connected agents). On the
twilio-primary-defaultcluster, use a live general-purpose queue such asgeneral-001— thedefaultqueue is paused and has no agents, so jobs sent there hang in "queuing". - Store the deploy secrets in AWS Secrets Manager (region
us-west-2, account058449100246/ success-write) and let the pipeline read them at deploy time by assuming an IAM role via the Buildkite OIDC provider — no static keys, no Buildkite-stored secrets:segment/source-function-template/public-api-token→{ "PUBLIC_API_TOKEN": "..." }(shared across all envs).segment/source-function-template/<dev|qa|prod>/function-id→{ "FUNCTION_ID": "sfnc_...", "SOURCE_ID": "sfn_..." }(per env; holds BOTH ids the two-call deploy needs).- IAM role
twilio-internal_source-function-templateis assumed by theaws-assume-role-with-web-identityplugin; its trust policy is scoped to this pipeline's slug and it may only read these secrets. (Created manually in success-write, mirroringdestination-function-template— not in Terraform.) - The deploy step sources
.buildkite/fetch-secrets.shto loadPUBLIC_API_TOKEN,FUNCTION_ID, andSOURCE_IDfor the targetDEPLOY_ENV.
- Promote the same way:
!!_RELEASE_TO_QAlabel → QA step; merge tomain→ PROD step. Per-step commit statuses (buildkite/<slug>/dev,/qa,/prod) gate the PR merge.
- Jest for code testing (Istanbul/
babelcoverage provider for honest branch coverage) - Prettier for code formatting
- ESLint (flat config) for code linting
- Buildkite pipeline for function deploy
- Husky for commit validation
