๐ŸŽ“ Class: how a version reaches production โ€” Cross CI/CD

YouGitHubCross TeamCityRegistryRelease botProductionChecks
01 / 18

Welcome: one version number's journey

๐Ÿ‘ฉโ€๐Ÿ’ป Youbump version+ merge๐Ÿญ Image buildCross TeamCitybuilds + receipt๐Ÿค– Release botopens theproduction PRโœ… Youreview +merge PR๐Ÿš€ Deploy + gatechecks, thenruns in prodOne version number travels left โ†’ rightYou do 3 things (orange). Everything else is automatic and double-checked.click 1clicks 2 + 3
  • Goal: a code change reaches production safely
  • You do only three things: bump, review, merge
  • Everything else is automatic, and every step is checked
Read the narration

Welcome to class. Today we follow one single thing: a version number. It starts on your laptop and ends running in production. Along the way it passes through GitHub, two TeamCity servers, a registry, a bot, and a security gate. The big idea is simple. You only do three things: you bump the version, you review a pull request, and you click merge. Everything else happens by itself. And every automatic step is checked by another step that does not trust it. Let's meet the pieces one by one.

02 / 18

The map: where every piece lives

GitHubService reposuser-api, bank-conciliationโ€ฆ(cross-exchange, cross-helper)production-deploymentcross-exchange/โ€ฆ"what runs in prod"Bot forkcross-release-bot/production-deploymentCross TeamCity (cross-server)Image builds16 services + 3 helpersRelease PR buildthe botRegistryregistry.cross-otc.comProduction clusterCrossProduction TeamCitydeploy builds per namespace+ release gatecross-production-deployerthe ONLY agent thatcan change productionYour appsnamespaces: cross,cross-minatech, cross-helpersRule #1: the factory (Cross TeamCity) holds NO key to production
  • GitHub: source repos, production-deployment, the bot's fork
  • Cross TeamCity (cross-server): builds images, runs the bot
  • Registry registry.cross-otc.com: stores images
  • Production cluster: CrossProduction TeamCity + deployer agent + your apps
Read the narration

Here is the map. Think of it as four neighborhoods. First, GitHub, where the code lives. There are the service repositories, like user-api or bank-conciliation, the production-deployment repository, which describes what runs in production, and a fork of it owned by a bot organization. Second, Cross TeamCity on cross-server. This is the factory. It builds images, and now it also runs the release bot. Third, the registry at registry dot cross-otc dot com. It is the warehouse where finished images are stored. Fourth, the production cluster. Inside it lives a second, separate TeamCity project called CrossProduction, with its own private deployer agent, and of course your running applications. The most important rule of this map: the factory never holds a key to production.

03 / 18

Step 1 โ€” You bump the version

Go APIscmd/server/main.go// @version 2.1.2websocket-gateway-apiswagger.yamlinfo.version: 2.1.2Web apps + helperspackage.json"version": "0.14.11"git push โ†’ masterservice repository on GitHubA version is published ONCE and never overwritten.
  • APIs: // @version in cmd/server/main.go
  • websocket-gateway-api: info.version in swagger.yaml
  • Web apps and helpers: version in package.json
  • Merge to the service repo's master
Read the narration

Step one is yours. You change a version number and merge to master in the service repository. Where is the version? For Go APIs it is the at-version comment in cmd server main dot go. For the websocket gateway it is info dot version in swagger dot yaml. For web apps and helpers like bank-conciliation, it is the version field in package dot json. Why a version number and not just the commit? Because a version is a promise: version 0.14.11 is published exactly once and never overwritten. A human readable number makes the pull request, the logs and the rollback obvious.

04 / 18

Step 2 โ€” The image build runs by itself

push to masterservice repoImage buildVCS trigger on mastertriggerTag exists? โ†’ skip โœ…green, publishes nothingNew version โ†’ buildamd64 + arm64 โ†’ pushdigest sha256:โ€ฆimage.json (the receipt)version: 0.14.11image: โ€ฆ@sha256:d7e5โ€ฆcommit: 69ed849โ€ฆwritesRegistrystores the imageby digestpushRemember the receipt: the bot reads it, and the gate re-checks it.
  • TeamCity watches each repo's master (VCS trigger)
  • Version already in registry โ†’ skip, green, publishes nothing
  • New version โ†’ build amd64 + arm64, push, compute digest
  • Writes image.json: the build's receipt
Read the narration

Step two. Cross TeamCity watches the master branch of every service. When you push, the build for that service starts by itself. First it reads the version and asks the registry: does this tag already exist? If yes, it says skipping, finishes green, and publishes nothing. That is why pushing a commit without a version bump is harmless. If the version is new, it builds the image for both A M D 64 and A R M 64, pushes it, and calculates the digest. A digest is a fingerprint, a long sha256 code that changes if even one byte of the image changes. Finally it writes a small file called image dot json. Think of image dot json as a receipt: it says which version, which digest, and which commit were published by this exact build. Remember that receipt, it comes back twice.

05 / 18

Why digests, not tags

๐Ÿท๏ธ Tag :0.14.11a label stuck on a boxcan be moved to another box๐Ÿงฌ Digest sha256:d7e5โ€ฆcomputed from the contentchange 1 byte โ†’ new digestWhat production pinsregistry.cross-otc.com/cross-helpers/bank-conciliation:0.14.11@sha256:d7e56e0bโ€ฆfor humansfor the machineKubernetes pulls by digest โ€” the version text cannot trick it.
  • A tag like :0.14.11 is a label โ€” it can be moved
  • A digest sha256:โ€ฆ is the content itself โ€” it cannot lie
  • Production pins BOTH: repo:0.14.11@sha256:โ€ฆ
  • The version is for humans, the digest is for the machine
Read the narration

A quick but important side lesson. Why do we care about digests? A tag, like colon 0.14.11, is just a label stuck on a box. Someone could move that label to a different box. A digest is computed from the content of the box itself, so it cannot lie. If anyone swaps the image, the digest no longer matches. That is why production pins both: the repository, then colon version, then at sha256 digest. The version is for you to read. The digest is for the machine to verify. Kubernetes pulls by the digest, so the version text cannot trick it.

06 / 18

Step 3 โ€” The release bot opens a pull request

Triggersafter every image build+ every hour at :07๐Ÿค– Release PR buildtools/preview release-prNewest image.jsonper build, from Cross TeamCity(read with build login)images.lock on masterproduction-deployment(read with pr App)readreadnewer than master?compare versions (never downgrade)commit on the forkpush App โ†’ fork onlyopen / refresh ONE PRpr App โ†’ production-deploymentyesyesalready on master โ†’ close old PR
  • Build: Release PR โ€” propose production releases
  • Runs after every image build and every hour at :07
  • Compares newest image.json with apps/<namespace>/images.lock on master
  • Newer? Commit on the fork, open or refresh ONE PR per service
Read the narration

Step three is the new robot. In Cross TeamCity there is a build called Release PR. It runs right after any image build succeeds, and also every hour at minute seven. It does one comparison. On one side, the newest version each build published, read from those image dot json receipts. On the other side, what production master currently pins, read from the images dot lock file of each namespace. If TeamCity has something newer, the bot writes a new images dot lock, commits it on its own fork, and opens one pull request per service on production-deployment. If a pull request is already open, it refreshes it on top of the latest master. If master already has that version, it closes the old pull request. And it never proposes a downgrade.

07 / 18

What the pull request actually changes

apps/cross-helpers/images.lockโˆ’ โ€ฆbank-conciliation:0.14.10@sha256:9a1fโ€ฆ+ โ€ฆbank-conciliation:0.14.11@sha256:d7e5โ€ฆthe whole release = a one-line diffapps/cross-helpers/โ€ฆ/server.yamlimage: "@IMAGE:cross-helpers/bank-conciliation@"placeholder, never a real image nameAt deploy: placeholder โ† line from the lockrelease_gate.py renderbinance-adapter-api โ†’ 1 PR edits 2 locksapps/cross + apps/cross-minatech
  • apps/cross/images.lock, apps/cross-minatech/images.lock, apps/cross-helpers/images.lock
  • One line per image: name registry/path:version@sha256:digest
  • Manifests say @IMAGE:cross-exchange/user-api@ โ€” the lock fills it in
  • binance-adapter-api is in two namespaces โ†’ one PR edits both
Read the narration

What does that pull request change? Only images dot lock files. There is one per namespace: cross, cross minatech, and cross helpers. Each line has a name and the full pinned reference, version and digest. The Kubernetes yaml files never contain real image names. They contain placeholders like at IMAGE colon cross-exchange slash user-api at. At deploy time, the placeholder is replaced with the line from the lock. So the whole release is a one line diff that you can read in two seconds. Binance adapter api runs in two namespaces, cross and cross minatech, so its single pull request edits both locks together. One service, one pull request, always.

08 / 18

Why a fork and a separate bot organization?

org: cross-release-botThe fork ๐Ÿดcross-release-bot/production-deploymentbot can write here โ€” nothing deploys from itbranch release/<service>one commit on top of masterrecreated every runorg: cross-exchange๐Ÿ›๏ธ production-deploymentbot CANNOT write hereonly YOU mergePull request"a suggestion" until you mergePR from forkmerge (you)Free GitHub plan = no branch protection โ†’ so the bot gets no write power at all
  • The bot must NOT be able to write to production-deployment
  • So it writes to its own copy: cross-release-bot/production-deployment
  • A PR from a fork is just a suggestion โ€” only you can accept it
  • Separate org = the bot's write power lives outside cross-exchange
Read the narration

Now, why a fork? Why not let the bot push a branch directly to production-deployment? Because any token that can push branches to that repository is one step away from pushing to master. GitHub's free plan does not let us block that with branch protection. So we took the write power away completely. The bot owns its own copy of the repository, in a separate organization called cross release bot. It can write there freely, because nothing deploys from there. Then it opens a pull request from the fork into the real repository. A pull request from a fork is only a suggestion. The real repository does not change until you merge. And because the fork lives in a different organization, the bot's write permission does not exist anywhere inside cross exchange.

09 / 18

The two bot GitHub Apps

๐Ÿ”ง cross-release-pushowner: cross-release-bot โ€ข App ID 5395232installed on: ONLY the forkContents: read & writewebhook off โ€ข private๐Ÿ“ cross-release-prowner: cross-exchange โ€ข App ID 5395312installed on: ONLY production-deploymentContents: read โ€ข Pull requests: read & writewebhook off โ€ข privateIf stolenโ€ฆcan scribble on the forkcannot touch productionIf stolenโ€ฆcan open/close PRscannot push code, cannot mergeEven BOTH together cannot change production โ€” only your merge can.
  • cross-release-push (owner cross-release-bot): Contents write, ONLY the fork
  • cross-release-pr (owner cross-exchange): Contents read + Pull requests write, ONLY production-deployment
  • Neither can merge. Neither can push to production-deployment
  • Webhooks off, private apps, installed on one repo each
Read the narration

The bot uses two GitHub Apps, and splitting them is on purpose. The first is cross release push. It belongs to the bot organization and it can write file contents, but it is installed on exactly one repository: the fork. The second is cross release pr. It belongs to cross exchange and it can read contents and write pull requests, installed only on production-deployment. Write pull requests means it can open, edit and close pull requests. It does not mean it can push code or merge. So if someone stole the push app, they could only scribble on the fork. If someone stole the pr app, they could only open pull requests that you still have to merge. Neither app alone, and not even both together, can change production. Both are private, have webhooks turned off, and each sees one single repository.

10 / 18

Every token, explained

Cross TeamCity๐Ÿญ Image buildsrun YOUR deps (npm, go)NO GitHub tokenNO prod access๐Ÿค– Release PR buildonly our own Go tool runsCROSS_RELEASE_PUSH_TOKENCROSS_RELEASE_PR_TOKEN(build-scoped, die at end)TEAMCITY auth.userId/password(per-build REST login)ssh-agent ci-sharedread-only deploy keyfor private Go modulesCrossProductionDeploy buildsCROSS_PRODUCTION_GITHUB_TOKEN(app cross-otc-production-teamcitypull requests: read)Record readerCROSS_RECORD_READER_TOKENuser production-record-reader(view Cross only)Build-scoped = minted when the build starts, revoked when it ends. Nothing long-lived on disk.Third-party code and tokens never share a build.
  • Build-scoped GitHub App tokens: created when a build starts, revoked when it ends
  • Release PR build: CROSS_RELEASE_PUSH_TOKEN + CROSS_RELEASE_PR_TOKEN
  • Release PR build also uses TeamCity's own per-build REST login to read image.json
  • Image builds get NO GitHub token at all
Read the narration

Let's list every token, because tokens are where security lives. Our GitHub tokens are build scoped tokens. TeamCity asks the GitHub App for a fresh token when a build starts, and the token is revoked when the build ends. Nothing long lived sits on disk. The Release PR build gets two of them: the push token for the fork, and the pr token for production-deployment. To read the image dot json receipts, it uses the temporary login that TeamCity gives every running build, so there is no stored TeamCity password either. And here is the key decision: the image builds, the ones that run your code and npm and go modules, get no GitHub token at all. Code that you did not write, like a dependency, runs during image builds. So we keep the bot in a completely separate build, where only our own tool runs.

11 / 18

Where the secrets are stored in TeamCity

Cross TeamCity โ†’ Root project โ†’ Connections๐Ÿ” cross-release-push (PROJECT_EXT_5)App ID, Client ID, client secret,private key โ€” encrypted in TeamCity๐Ÿ” cross-release-pr (PROJECT_EXT_6)App ID, Client ID, client secret,private key โ€” encrypted in TeamCityProject Cross (settings in git)cross.release.push-connection= PROJECT_EXT_5 (just an id)cross.release.pr-connection= PROJECT_EXT_6 (just an id).pem files deleted from your laptop โ€ข no secrets in gitEach App has TeamCity's unique callback URL registered
  • Root project โ†’ Connections: cross-release-push (PROJECT_EXT_5), cross-release-pr (PROJECT_EXT_6)
  • Private keys + client secrets live encrypted in TeamCity, .pem files deleted
  • Project Cross is versioned in git โ†’ parameters point at the connection ids
  • Callback URL on each App = TeamCity's unique callback
Read the narration

Where do the app secrets live? In Cross TeamCity, in the Root project, under Connections. There are two connections, cross release push with id project ext 5, and cross release pr with id project ext 6. Each one stores the app id, client id, client secret and private key, encrypted inside TeamCity. The downloaded pem key files were deleted from your laptop. Why the Root project and not the Cross project? Because the Cross project's settings come from git, so its connection page is read only, and we did not want secrets in git. The Cross settings in git only contain two harmless parameters that point to those ids. Each app also has TeamCity's unique callback address registered, which is required for the connection test to pass.

12 / 18

Step 4 โ€” You review and merge

๐Ÿ“ PR: Release bank-conciliation 0.15.0Namespace โ”‚ Image โ”‚ Version โ”‚ Digest โ”‚ Buildcross-helpers โ”‚ cross-helpers/bank-conciliation โ”‚ 0.14.11 โ†’ 0.15.0 โ”‚ sha256:9c2eโ€ฆ โ”‚ #352Source commit: 4f1a2b3c4d5eOpened by the Cross release bot. Merging deploys the changed namespaces.Merge = approve โœ…must be kevynsaxClose = reject โŒbot will not reopenthe same versions
  • PR body: namespace, image, old โ†’ new version, digest, TeamCity build link, commit
  • Merge = approval. Close = rejection (bot won't reopen the same versions)
  • Must be merged by kevynsax โ€” the gate checks it
  • Free plan has no branch protection, so the gate enforces this instead
Read the narration

Step four is you again. The pull request shows a small table: the namespace, the image, the old version, the new version, the digest, a link to the TeamCity build that produced it, and the source commit. If you like it, you merge. Merging is the approval. If you do not want it, you close it, and the bot will not reopen it for the same versions. Only a newer version brings it back. One more rule: it must be merged by you, kevynsax. GitHub's free plan cannot enforce that with branch protection, so the production gate enforces it instead, as you will see in a second.

13 / 18

Step 5 โ€” The deploy starts by itself

merge lands on masterproduction-deploymentCrossProduction triggers+:/apps/cross/**+:/apps/cross-minatech/**+:/apps/cross-helpers/**Only changed namespacebank-conciliation โ†’ cross-helpers๐Ÿ›ก๏ธ Agent: cross-production-deployerlives INSIDE the production clusterthe only agent allowed to deployruns on๐Ÿ”‘ cross-otc-production-teamcity: pull requests read (merge history)
  • CrossProduction TeamCity watches production-deployment master
  • Trigger rule: +:/apps/<namespace>/** โ€” only that namespace deploys
  • Runs ONLY on the cross-production-deployer agent inside the cluster
  • Uses app cross-otc-production-teamcity (pull requests: read) to read history
Read the narration

Step five. The merge lands on production-deployment master. The CrossProduction TeamCity project watches it. Each namespace has its own deploy build, and its trigger only fires when files under apps slash that namespace change. So merging a bank-conciliation release only deploys cross helpers, nothing else. This build only runs on one special agent, the cross production deployer, which lives inside the production cluster and is the only thing with permission to change it. To read the merge history it uses a third GitHub App, cross otc production teamcity, with read access only. Again, a build scoped token that disappears when the build ends.

14 / 18

Step 6 โ€” The release gate: trust nothing

๐Ÿšง Release gateci/release_gate.py checkCheck A โ€” who merged?walk every master commit sincelast deploy (via GitHub API)must be a PR merged by kevynsaxCheck B โ€” is the digest real?for each changed lock line: find theTeamCity build for that versiondigest must equal its image.json๐Ÿ”‘ production-record-reader (view Cross only)Both pass โ†’ render, apply, rolloutrecord revision in ConfigMap production-deploy-stateany mismatch โ†’ REFUSE
  • Check A: every master commit since the last deploy is a PR merged by kevynsax
  • Check B: every changed digest matches the image.json of the TeamCity build that published it
  • Reads TeamCity as production-record-reader (view-only on project Cross)
  • Any mismatch โ†’ refuse to deploy. Pass โ†’ render, apply, record revision
Read the narration

Step six is the guard at the door, called the release gate. Before deploying, it trusts nobody, not even the bot. Check A: it walks back through every commit on master since the last deploy, and each one must be a pull request merged by kevynsax. A direct push, or a merge by someone else, stops everything. Check B: for every image line whose digest changed, it goes to Cross TeamCity, finds the build that published that version, and compares the digest with the image dot json receipt. Remember the receipt? This is where it comes back. If the bot was hacked and wrote a fake digest, the receipt would not match, and the deploy is refused. To read those receipts it uses a special user, production record reader, who can only view the Cross project, and its token is stored as a password parameter in CrossProduction. If both checks pass, the gate fills in the placeholders, applies the manifests, waits for the rollout, and records this revision as the new last deployed point.

15 / 18

The security walls, all together

Buildersrun untrusted depsno prod credscross-teamcity appREMOVED fromproduction-deploymentOne deployer agentinside the clusteronly after the gateTwo proofsyour merge +TeamCity receiptwall 1wall 2wall 3wall 4To ship an evil image, an attacker must break YOU and TeamCity at the same time.
  • Builders have NO production credentials
  • cross-teamcity app was removed from production-deployment
  • Only one agent can deploy, and only after the gate
  • Two independent proofs: your merge + TeamCity's receipt
Read the narration

Let's step back and see the walls. Wall one: the builders, the machines that run untrusted dependency code, hold no production credentials at all. Wall two: the cross teamcity GitHub app, used by the factory, was removed from production-deployment, so the factory cannot even read the production repository. Wall three: only one agent inside the cluster can deploy, and only after the gate passes. Wall four: two independent proofs are needed for any image to reach production. Your merge, checked through GitHub, and TeamCity's receipt, checked through a read only user. An attacker would need to break you and break TeamCity at the same time.

16 / 18

What ifโ€ฆ? (attack drills)

๐Ÿ˜ˆ Bot tokens stolenopens PRs / writes fork๐Ÿ›ก๏ธ Resultyou don't merge โ†’ nothing deploys๐Ÿ˜ˆ Fake digest in PRpoints to an evil image๐Ÿ›ก๏ธ Resultgate vs image.json โ†’ REFUSED๐Ÿ˜ˆ Direct push to masterskips review๐Ÿ›ก๏ธ Resultgate: not your PR merge โ†’ REFUSED๐Ÿ˜ˆ Malicious npm/go packageruns in an image build๐Ÿ›ก๏ธ Resultno token there to steal
  • Bot token stolen โ†’ can open PRs; you don't merge; nothing deploys
  • Fake digest in a PR โ†’ gate compares with image.json โ†’ refused
  • Someone pushes to master directly โ†’ gate: not a PR merge โ†’ refused
  • Malicious npm package in a build โ†’ no GitHub or production token to steal
Read the narration

Let's practice with some what ifs. What if the bot's tokens are stolen? The thief can open pull requests and write to the fork. You don't merge, so nothing deploys. What if a pull request contains a fake digest pointing to an evil image? The gate compares it with the TeamCity receipt, it does not match, deploy refused. What if someone pushes straight to master? The gate sees a commit that is not a pull request merged by you, deploy refused. What if a malicious package sneaks into an image build? That build has no GitHub token and no production access, so there is nothing to steal. Each layer assumes the one before it might fail.

17 / 18

Why these choices (and what we rejected)

CHOSENWHYREJECTEDBot = separate buildtokens never meet third-party codebot in the image buildFork + 2 small Appsno write power on production repo1 App with write accessVerify vs TeamCity image.jsononly the real build writes receiptstrust the registryHourly scheduleno new repo access for Cross TCgit watcher on prod repoConnections in Rootsecrets stay out of gitsecrets in versioned DSLPrinciple: smallest power each piece needs + a second piece that checks it
  • Bot as a separate build โ†’ tokens never meet third-party code
  • Fork + 2 Apps instead of 1 App with write โ†’ no write power on production repo
  • Verify against TeamCity image.json, not the registry โ†’ registry push โ‰  proof of origin
  • Hourly schedule instead of a git watcher โ†’ no new repo access for Cross TeamCity
  • Connections in Root project โ†’ secrets stay out of git
Read the narration

Finally, why these choices. We run the bot as a separate build, so its tokens never sit next to third party code. We used a fork and two apps instead of one app with write access, so nothing in the bot can write to the production repository. The gate checks the TeamCity receipt instead of just asking the registry, because anyone with a registry password could push an image, but only the real build writes the receipt. The bot refreshes on an hourly schedule instead of watching production master with git, so Cross TeamCity did not need any new access to that repository. And the app secrets live in the Root project connections, so they stay out of git. Every choice follows one principle: give each piece the smallest power it needs, and make a second piece check it.

18 / 18

Recap: your three clicks

๐Ÿ‘ฉโ€๐Ÿ’ป Youbump version+ merge๐Ÿญ Image buildCross TeamCitybuilds + receipt๐Ÿค– Release botopens theproduction PRโœ… Youreview +merge PR๐Ÿš€ Deploy + gatechecks, thenruns in prodOne version number travels left โ†’ rightYou do 3 things (orange). Everything else is automatic and double-checked.click 1clicks 2 + 3
  • 1. Bump the version and merge in the service repo
  • 2. Review the bot's PR on production-deployment
  • 3. Merge it โ€” the deploy and the gate do the rest
  • First run will propose KYC 4.0.1 twice: close those
Read the narration

Recap. Your whole job is three clicks. One: bump the version and merge in the service repository. Two: review the pull request the bot opens on production-deployment. Three: merge it. The image build, the bot, the deploy, and the gate do the rest, and they check each other the whole way. One practical note: the first time the bot runs, it will propose kyc api and kyc web app 4.0.1, because production intentionally stays on 2.0.0. Just close those two pull requests, and the bot will leave them alone until a newer KYC version appears. That's the class. Thanks for listening.

Cheat sheet โ€” every piece in one place

Repositories

RepoWhat it isWho can write
cross-exchange/<service>, cross-helper/<helper>Service source code + version numberYou (merge)
cross-exchange/deploymentCross TeamCity DSL, build scripts, release-pr bot (tools/preview/release.go)You (merge)
cross-exchange/production-deploymentapps/<ns>/*.yaml + images.lock, release gate, CrossProduction DSLOnly your merges (gate enforces)
cross-release-bot/production-deploymentBot fork; branches release/<service>cross-release-push App
kevynsax/production-deploymentYour personal fork (manual PRs)You

GitHub Apps

AppOwnerInstalled onPermissionsUsed by
cross-release-push (5395232)cross-release-botthe fork onlyContents R/W, Metadata RRelease PR build
cross-release-pr (5395312)cross-exchangeproduction-deployment onlyContents R, Pull requests R/W, Metadata RRelease PR build
cross-otc-production-teamcitycross-exchangeproduction-deploymentContents R, Pull requests RCrossProduction deploy builds (gate)
cross-teamcitycross-exchangeservice repos (NOT production-deployment)checkoutCross TeamCity image builds

Tokens & credentials

NameKindLifetimeWhere
CROSS_RELEASE_PUSH_TOKENGitHub App build-scoped token (connection PROJECT_EXT_5)one buildRelease PR build
CROSS_RELEASE_PR_TOKENGitHub App build-scoped token (connection PROJECT_EXT_6)one buildRelease PR build
teamcity.auth.userId / passwordTeamCity per-build REST loginone buildRelease PR build (reads image.json)
CROSS_PRODUCTION_GITHUB_TOKENbuild-scoped token, app cross-otc-production-teamcityone buildProduction deploy (gate check A)
CROSS_RECORD_READER_TOKENTeamCity token of user production-record-reader (view Cross)long-lived, password param in CrossProductionProduction deploy (gate check B)
ci-shared / ci-<repo> SSH keysread-only deploy keyslong-lived in TeamCityImage builds (checkout, Go modules)

TeamCity builds

BuildServerTriggerDoes
Cross_<Service>_Build (ร—19)Cross TeamCitypush to service masterskip existing version or build + push + image.json
Cross_ReleasePullRequestsCross TeamCityafter any image build + hourly :07open/refresh/close release PRs
CrossProduction_Deployment_Deploy{Cross,CrossMinatech,CrossHelpers}CrossProductionmerge touching apps/<ns>/**release gate โ†’ render โ†’ apply โ†’ record

What's pending

  • Merge deployment PR #26 to turn the bot on.
  • Run Cross_ReleasePullRequests once; close the two KYC 4.0.1 PRs.
  • Decide how deploy/env.yaml values flow into production manifests.
0:00 / 0:00