For maintainers. Using T3 Code? See docs/user.
This document covers the unified release workflow for stable and nightly desktop releases.
- Workflow:
.github/workflows/release.yml - Triggers:
- manual
workflow_dispatchwithchannel=stable, the normal way to ship stable. Stable and nightly dispatches must selectmain; preview may select any branch. The channel defaults to preview so an omitted selection cannot publish a stable release. - push tag matching
v*.*.*for a stable release of an explicit commit - scheduled nightly check every 30 minutes
- manual
workflow_dispatchwithchannel=nightly - manual
workflow_dispatchwithchannel=preview, the maintainers' test train. It exercises the whole release flow (build, sign, notarize, smoke, publish) for a commit that end users must never receive, which is how an unmerged branch or a risky change gets a real release run before it lands. It builds the triggering commit with nightly's versioning under thepreviewprerelease identifier (0.0.41-preview.<date>.<run>) and publishes a GitHub prerelease plus the npm packages under thepreviewdist-tag. Preview is not on the schedule, no default npm dist-tag points at it, its desktop builds carry no update feed, and no updater manifest (latest*.yml,nightly*.yml, blockmaps) is attached, so a stable or nightly install cannot be offered one. The only ways onto it are downloading the release by hand,npx t3@preview,T3CODE_CHANNEL=previewfor the install scripts, ort3 update --channel previewfrom a terminal; each prints a warning, and the CLI asks for confirmation when the running build is not itself a preview. The release itself is named as a maintainer test build and its body is a warning rather than generated notes: a changelog of unmerged branch history is not a changelog, and nightly and stable notes are unaffected because each series resolves its previous tag within its own channel. The hosted web app, AUR, and Discord announcements are skipped. Keep it; it costs nothing when idle.
- manual
- A manual stable release builds the commit of the latest published nightly, not
mainHEAD. Nightly is the release candidate: verify the nightly, then promote it. Merges tomainkeep landing while you verify and never leak into the stable build.- The version defaults to the one the nightly previewed (
0.0.39-nightly.*ships as0.0.39). Pass theversioninput to override it, for example for a minor bump. - The stable tag is created on the nightly's commit when the GitHub Release is published.
- Pushing a
vX.Y.Ztag by hand still works and builds exactly the tagged commit. Use it when the commit to ship is not the latest nightly, such as a cherry-picked fix on a release branch.
- The version defaults to the one the nightly previewed (
- Runs lint, typecheck, and tests alongside artifact builds. Publishing waits for every check.
- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients.
- Builds the platform-independent JS (server bundle, web client, Electron main) once in the
build_bundlejob and hands it to every platform job as thejs-bundleartifact; the platform jobs only package it, so no runner rebuilds it. - Builds six desktop artifacts in parallel for both channels, each as its own job (
desktop_<platform>_<arch>, one call ofrelease-desktop.yml) on hardware of its own architecture, gated only on the bundle (the Windows jobs also wait for the same-arch Linux job, whose CLI archive they embed as the WSL runtime):- macOS
arm64DMG - macOS
x64DMG - Linux
x64andarm64AppImage - Windows
x64andarm64NSIS installer
- macOS
- Publishes one GitHub Release with all produced files.
- Stable tags with a suffix after
X.Y.Z(for example1.2.3-alpha.1) are published as GitHub prereleases. - Only plain stable
X.Y.Zreleases are marked as the repository's latest release. - Nightly runs are always GitHub prereleases and never marked latest.
- Automatically generated release notes are pinned to the previous tag in the same channel, so stable compares to the previous stable tag and nightly compares to the previous nightly tag.
- Stable tags with a suffix after
- Includes Electron auto-update metadata (for example
latest*.yml,nightly*.yml, and*.blockmap) in release assets. - Builds a self-contained CLI archive per platform (
t3-<version>-<platform>-<arch>.tar.gz,.zipon Windows) in the same job as that target's desktop artifact and attaches them to the GitHub Release with aSHA256SUMSfile, on every channel, for five targets: macOS arm64, Linux x64 and arm64, Windows x64 and arm64. Every archive is built, signed, and smoke-tested on hardware of its own architecture. There is no macOS x64 archive: Node single-executables are unsupported on x64 macOS (the SEA docs list macOS as arm64 only) and the binary segfaults on start; the x64 desktop app is Electron and unaffected.- The archive holds the server as a Node single-executable (
scripts/build-cli-archive.ts), so unpacking it needs neither Node, npm, nor a compiler. It is the only form in which T3 Code manages a runtime: the desktop's SSH environments, the boot service,t3 update, and the install scripts all download and verify this archive againstSHA256SUMS. The npm packages exist for people who runnpx t3ornpm install -g t3themselves and carry the same archive contents; nothing in the product installs from npm. Thecurl | shinstallers arescripts/install.shandscripts/install.ps1; the marketing site copies them into itspublic/at build time (apps/marketing/scripts/stage-install-scripts.mjs) and serves them att3.codes/install.shand/install.ps1. - The executable is built with a Node that supports
--build-sea(VP_NODE_VERSION=26.8.2, kept in step withSEA_NODE_VERSIONinapps/server/vite.config.ts), while the repo stays onengines.node. - macOS archives are signed with the Developer ID certificate and notarized when the Apple secrets are present (ad hoc otherwise, which still runs from
curl/tarinstalls). Windows executables use the same Azure Trusted Signing setup as the installer. Every native addon in the macOS archive is signed too, since the hardened runtime refuses unsigned libraries. - Each archive is extracted and executed on its build runner (
scripts/smoke-cli-archive.ts) before it is uploaded.
- The archive holds the server as a Node single-executable (
- Publishes the CLI to npm with OIDC trusted publishing from the same workflow file, as the same bytes the GitHub Release carries:
scripts/build-npm-platform-packages.tsunpacks the five CLI archives into@t3code/t3-<platform>-<arch>packages (each withos/cpuset so npm installs only the matching one) and generates thet3launcher, whosebin/t3.jslists them asoptionalDependenciesand execs the installed executable.npx t3therefore needs Node only to run the launcher, never to run the server.node apps/server/scripts/cli.ts publishpublishes the platform packages first and the launcher last, after a--dry-runpass over all of them so an auth or scope error fails before anything is live.- stable releases publish npm dist-tag
latest - nightly releases publish npm dist-tag
nightly - preview releases publish npm dist-tag
preview, which nothing resolves unless asked for by name - one-time setup: the
@t3codenpm scope (org) must exist, andt3and each@t3code/t3-<platform>-<arch>package needs a trusted publisher registered for this workflow file (see below).
- stable releases publish npm dist-tag
- Deploys the hosted web app to Vercel only after a release is published:
- stable releases are aliased to the
latesthosted app channel - nightly releases are aliased to the
nightlyhosted app channel
- stable releases are aliased to the
- Signing is optional and auto-detected per platform from secrets.
Labeling a PR preview:mac publishes a signed, notarized Apple Silicon DMG with T3 Connect enabled
to the rolling desktop-preview prerelease, and works for fork PRs. The label is a one-shot request
for the commit it is applied to: the trusted workflow removes it once the build is in hand, and later
pushes do not build until a maintainer applies it again. Every signed preview is therefore a
per-commit maintainer decision, which matters because the result carries the Developer ID signature.
Vouching a contributor lets their labeled commits be signed; it is not a standing grant. The build is
split so the Developer ID certificate never shares a job with PR code:
.github/workflows/desktop-macos-preview.ymlruns onpull_requestwith no secrets and builds only the JS bundle from the PR (the samejs-bundleartifactrelease.ymlproduces)..github/workflows/desktop-macos-preview-publish.ymlruns onworkflow_runfrommain. It refuses unless the PR is open, still labeled, its head is the built commit, and the author is a bot, a collaborator, or listed in.github/VOUCHED.td(read from the default branch, so a PR cannot vouch for itself). It then packages and signs the bundle throughrelease-desktop.ymlchecked out atmain, so packaging, native helpers, and the Electron/desktop dependencies come frommain, not the PR. Only the version and the public T3 Connect identifiers in.env.exampleare read from the PR commit, as data, so the signed app's passkey entitlement matches the bundle. A PR that changes packaging must use thechannel=previewrelease train above instead.
Before handing the bundle to the signing runner, the trusted workflow validates its ZIP entries
and accepts only regular files under server/dist and desktop/dist-electron, plus the directory
entries that lead to those roots. The artifact cannot
overwrite packaging code or installed dependencies. The bundle is copied into the app, never executed,
on the signing runner. The
pull_request_target cleanup job in the publish workflow removes the download when the PR closes, or
when the label is removed by hand before a build consumed it, and never checks out PR code.
Stable releases require these GitHub Actions secrets in addition to the platform and deployment credentials documented below:
RELEASE_APP_IDRELEASE_APP_PRIVATE_KEY
The finalize job uses them to commit and push aligned package versions to main as the Release App.
GitHub Release publication uses the repository-scoped workflow token so it has a rate-limit quota
independent from the shared Release App installation.
The relay is a shared control plane versioned separately from client releases. Stable and nightly client builds must point at the same relay so users see the same linked environments when switching release channels.
.github/workflows/deploy-relay.yml deploys Alchemy stage prod on every push to main. The
release workflow reads the relay URL and Clerk client configuration from the existing production
GitHub Actions environment before building desktop, CLI, or hosted web artifacts.
Required repository variables shared by relay deployments:
CLOUDFLARE_ACCOUNT_IDPLANETSCALE_ORGANIZATIONAXIOM_ORG_ID
Required repository secrets shared by relay deployments:
CLOUDFLARE_API_TOKENPLANETSCALE_API_TOKEN_IDPLANETSCALE_API_TOKENAXIOM_TOKEN
Required production environment variables:
RELAY_API_ZONE_NAMERELAY_TUNNEL_ZONE_NAMECLERK_PUBLISHABLE_KEYCLERK_JWT_AUDIENCECLERK_JWT_TEMPLATECLERK_CLI_OAUTH_CLIENT_IDAPNS_ENVIRONMENTAPNS_TEAM_IDAPNS_KEY_IDAPNS_BUNDLE_ID
Optional production environment variables:
RELAY_DOMAINwhen overriding the derivedrelay.<RELAY_API_ZONE_NAME>domain
Required production environment secrets:
CLERK_SECRET_KEYAPNS_PRIVATE_KEY
The account-scoped repository credentials are consumed by Alchemy while provisioning relay stages; they
are not bound into the relay Worker. The production deployment uses an Axiom personal access token,
so AXIOM_ORG_ID must accompany AXIOM_TOKEN. The prod stage owns the retained PlanetScale
database. Local personal stages provision isolated branches from it and are never deployed by CI.
Production adopts the configured relay API and tunnel DNS zones as retained Cloudflare resources.
Personal stages reference the production-owned zones.
Developers deploy personal stages locally rather than through pull-request automation:
vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.localAfter a nightly release is published, the release workflow deploys the same commit to the marketing site's Vercel production project. Stable releases do not deploy the marketing site because they can promote an older nightly commit.
The job looks up the t3code-marketing project using the existing VERCEL_TOKEN
and VERCEL_ORG_ID secrets. It also respects the optional VERCEL_TEAM_SLUG
variable. The Vercel project's root directory must be apps/marketing.
Git deployments remain disabled in apps/marketing/vercel.ts.
The hosted app is intentionally not deployed by Vercel's Git integration. The
web project disables automatic Git deployments in apps/web/vercel.ts via
git.deploymentEnabled: false, and .github/workflows/release.yml deploys the
web app with Vercel CLI after the GitHub Release succeeds.
Required GitHub Actions secrets:
VERCEL_TOKENVERCEL_ORG_IDVERCEL_PROJECT_ID
Optional GitHub Actions variables:
VERCEL_TEAM_SLUG: overrides the Vercel CLI scope when the team slug is preferred over theVERCEL_ORG_IDsecret.T3CODE_WEB_ROUTER_URL: defaults tohttps://app.t3.codes.T3CODE_WEB_LATEST_DOMAIN: defaults tolatest.app.t3.codes.T3CODE_WEB_NIGHTLY_DOMAIN: defaults tonightly.app.t3.codes.
Required Vercel domains:
app.t3.codes: the router domain users open, updated by stable releases.latest.app.t3.codes: channel alias updated by stable releases.nightly.app.t3.codes: channel alias updated by nightly releases.
The router domain uses apps/web/vercel.ts routes. Users opt into a channel by
visiting /__t3code/channel?channel=latest or
/__t3code/channel?channel=nightly; the router stores the
t3code_web_channel cookie and rewrites future requests on app.t3.codes to
the matching channel alias.
The release deploy job rewrites release package versions before upload so the
hosted app's About panel renders the release version. Stable deploys alias the
same deployment to both the latest channel and the router domain so the router
rules stay current. Nightly deploys only alias the nightly channel. The job
also passes VITE_HOSTED_APP_CHANNEL=latest|nightly, which renders the hosted
update track selector in the About panel. Changing the selector navigates
through /__t3code/channel on the router domain so the user's channel cookie is
updated before redirecting to the hosted app root.
One-time Vercel dashboard setup:
- Confirm the web project root directory remains
apps/web. - Add the three domains above to the web project.
- Disable automatic Git deployments in the dashboard if desired; the committed
vercel.tssetting is the source-of-truth, but disconnecting Git in the dashboard is also safe. - Run one stable release deployment, or manually alias the current stable
deployment, so
app.t3.codespoints at a deployment containing the router rules inapps/web/vercel.ts. Future stable releases keep this alias current.
- Workflow:
.github/workflows/release.yml - Triggers:
- scheduled check every 30 minutes
- manual
workflow_dispatchwithchannel=nightly
- Automatic nightlies require new commits and at least six hours since the last nightly was published, including manual nightlies.
- Manual nightlies bypass the time and change checks. Nightly runs remain serialized. Scheduled runs wait for an active nightly to finish, then check the publication gap before building.
- Runs the same desktop quality gates and artifact matrix as the tagged release flow.
- Publishes a GitHub prerelease only:
- current tag format:
vX.Y.Z-nightly.YYYYMMDD.<run_number> nightly-v...is accepted only as a legacy previous-nightly tag- release name includes the short commit SHA
make_latestis alwaysfalse
- current tag format:
- Uses the next stable patch version as the nightly base. For example,
0.0.17produces nightlies on0.0.18-nightly.*. - Publishes Electron auto-update metadata to the dedicated
nightlyupdater channel, so desktop users can opt into that track independently from stable. - Publishes the CLI npm packages (
t3and@t3code/t3-<platform>-<arch>) to thenightlynpm dist-tag using the same nightly version. - Does not commit version bumps back to
main.
Connected servers update to the client's exact version, not to an npm dist-tag. Every released
desktop or hosted client version must therefore have a matching t3@<version> package available on
npm before users can receive that client.
The workflow enforces this ordering:
publish_clipublishes the exact release version to npm, on every channel.releasedepends onpublish_clibefore exposing desktop artifacts in GitHub Releases.deploy_webdepends onreleasebefore moving the hosted channel to the new client.
Preserve these dependencies when changing the release graph. Publishing a client first would leave the Update server action targeting a package version that does not exist yet.
For a release smoke test, confirm npm view t3@<version> version returns the expected version, then
connect the new client to a server on the previous version and verify that the update action
reconnects to the matching server. When the release adds database migrations, verify that the
remote update applies them and reconnects. A failed trial must restore the database snapshot and
restart the previous server. If the installed launcher does not support the target protocol,
verify that the update stops before restart and run npx t3@<version> service update once on the
server machine. Also test the manual or desktop-managed guidance when those environments are
available.
- Updater runtime:
apps/desktop/src/updates/DesktopUpdates.ts. electron-updateradapter:apps/desktop/src/electron/ElectronUpdater.ts.apps/desktop/src/main.tsonly wires the updater layers into the desktop runtime.- Update UX:
- Background checks run on startup delay + interval.
- No automatic download or install.
- The desktop UI shows a rocket update button when an update is available; click once to download, click again after download to restart/install.
- Provider: GitHub Releases (
provider: github) configured at build time. - Repository slug source:
T3CODE_DESKTOP_UPDATE_REPOSITORY(formatowner/repo), if set.- otherwise
GITHUB_REPOSITORYfrom GitHub Actions.
- Required release assets for updater:
- platform installers (
.exe,.dmg,.AppImage, plus macOS.zipfor Squirrel.Mac update payloads) - channel metadata:
latest*.ymlfor stable releases,nightly*.ymlfor nightly releases *.blockmapfiles (used for differential downloads)
- platform installers (
- macOS metadata note:
electron-updaterreadslatest-mac.ymlon stable andnightly-mac.ymlon nightly, for both Intel and Apple Silicon.- The workflow merges the per-arch mac manifests into one channel-specific mac manifest before publishing the GitHub Release.
Windows packages the bundled server and only its runtime-external/native
dependency closure in resources/server.asar. Native modules and helper
executables declared as unpacked by that archive must be present at the matching
paths below resources/server.asar.unpacked. The Windows-native backend reads
the archive in place through Electron. Packaged Windows builds also ship
resources/wsl-runtime.tar.gz plus its SHA-256 sidecar: the Linux CLI archive
(t3-<version>-linux-<arch>.tar.gz, the same arch as the Windows host) built
by the Linux desktop job and handed to the Windows desktop build as
--wsl-runtime, copied in verbatim so WSL runs the exact bytes a Linux user
downloads. WSL verifies and extracts that archive
into ~/.t3/wsl-runtime/sha256-<archive-digest> inside the selected distro,
then reuses it for later launches of the same update.
Windows keeps JavaScript and package metadata inside app.asar and unpacks only
native libraries and helper executables. Avoid enabling whole-package smart
unpacking: each loose file adds work to NSIS installation and counts against
the payload limit.
The artifact builder rejects a Windows package when any of these invariants break:
resources/server.asaris absent or does not contain the server entry.- Any file marked unpacked in the ASAR header is absent from
resources/server.asar.unpacked. - On same-architecture Windows builds, the packaged primary cannot load the fff
native library from inside
server.asarthrough its.unpackedsibling. - The isolated, extracted sidecar cannot load the server entry with plain Node.
- A Windows build given
--wsl-runtimeomits the WSL archive or SHA-256 sidecar, or the sidecar digest does not match the emitted archive. - The emitted WSL archive is not a Linux CLI release archive: it must unpack to
a single
t3-<version>-linux-<arch>directory holdingt3,client/, andnode_modules/with the Linux node-pty binary, and must not carry a loose server bundle (bin.mjs). - The external Windows resource monitor is absent.
- The unpacked Windows application contains more than 80 files.
Cross-architecture Windows builds retain every structural and extracted-sidecar check, but skip executing the target Electron binary. A same-architecture build for each release target must exercise the primary native-load probe.
NSIS differential packaging remains enabled. A sidecar layout transition can produce a larger one-time download; subsequent small releases retain their blockmaps, with a 60 MB maximum for a representative sidecar-to-sidecar update.
The workflow runs node scripts/build-npm-platform-packages.ts on the downloaded CLI archives, then
node apps/server/scripts/cli.ts publish --packages-dir npm-packages, which runs npm publish on
each @t3code/t3-<platform>-<arch>.tgz and finally on t3.tgz, the launcher. The script publishes
tarballs it built itself rather than directories: npm publish <dir> strips node_modules/ from the
tarball no matter what files says, and the executable loads its native addons from there. Seven
packages are published per release: t3, @t3code/t3-darwin-arm64, @t3code/t3-darwin-x64,
@t3code/t3-linux-arm64, @t3code/t3-linux-x64, @t3code/t3-win32-arm64,
@t3code/t3-win32-x64.
Checklist:
- Confirm the npm org owns package
t3and the@t3codescope exists on npm (create the org if it does not). - For
t3and each@t3code/t3-<platform>-<arch>package, configure a Trusted Publisher in the npm package settings (a package that has never been published needs a first publish or a placeholder before the setting exists; the--dry-runstep inpublish_clireports which names are still rejected):- Provider: GitHub Actions
- Repository: this repo
- Workflow file:
.github/workflows/release.yml - Environment (if used): match your npm trusted publishing config
- Ensure npm account and org policies allow trusted publishing for every package.
- Create release tag
vX.Y.Zand push; workflow will:- build and smoke-test the five CLI archives
- build the npm packages from those archives
- publish them with npm dist-tag
latest
- Nightly runs publish with npm dist-tag
nightly; preview runs withpreview.
There is no dry-run tag path. Pushing any accepted non-nightly tag, including
v0.0.0-test.1, classifies the run as the stable channel. It publishes t3 with npm dist-tag
latest, creates a real GitHub Release, aliases the hosted app to latest.app.t3.codes and
app.t3.codes, and can commit a version bump to main in the finalize job. Do not push a test tag
to validate the workflow.
The workflow has no non-publishing workflow_dispatch mode. Use normal CI or local quality gates to
validate checks and builds without shipping. To exercise the complete release graph at lower stable
risk, manually dispatch channel=nightly; this still publishes a real nightly npm package, GitHub
prerelease, desktop updater release, hosted nightly alias, and marketing site, but it does not update stable app aliases or
commit a version bump to main. Only run it when a real nightly release is acceptable.
Manual channel=stable is also a real stable-channel release. Omitting signing secrets only makes
platform artifacts unsigned; it does not prevent publication.
Required secrets used by the workflow:
CSC_LINKCSC_KEY_PASSWORDAPPLE_API_KEYAPPLE_API_KEY_IDAPPLE_API_ISSUERMACOS_PROVISIONING_PROFILE(base64-encoded provisioning profile with Associated Domains)
Required repository variables:
APPLE_TEAM_ID
Optional repository variables:
CLERK_PASSKEY_RP_DOMAINS: comma-separated RP-domain override. By default, the build derives the domain from the production Clerk publishable key.
Checklist:
- Apple Developer account access:
- Team has rights to create Developer ID certificates.
- Create an explicit App ID for
com.t3tools.t3codeand enable Associated Domains. - Create a
Developer ID Applicationcertificate and a compatible provisioning profile for that App ID with Associated Domains enabled. - Export the certificate + private key as
.p12from Keychain. - Base64-encode the
.p12and store asCSC_LINK. - Base64-encode the provisioning profile and store it as
MACOS_PROVISIONING_PROFILE. - Store the
.p12export password asCSC_KEY_PASSWORD, and setAPPLE_TEAM_IDto the 10-character Apple Developer Team ID. - In App Store Connect, create an API key (Team key).
- Add API key values:
APPLE_API_KEY: contents of the downloaded.p8APPLE_API_KEY_ID: Key IDAPPLE_API_ISSUER: Issuer ID
- Complete the Clerk Native API and AASA setup in T3 Connect setup.
- Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected
com.apple.developer.associated-domainsentitlement.
Notes:
APPLE_API_KEYis stored as raw key text in secrets.- The workflow writes it to a temporary
AuthKey_<id>.p8file at runtime. - The workflow decodes
MACOS_PROVISIONING_PROFILE, validates it withsecurity cms, and passes it to the desktop packager.
Required secrets used by the workflow:
AZURE_TENANT_IDAZURE_CLIENT_IDAZURE_CLIENT_SECRETAZURE_TRUSTED_SIGNING_ENDPOINTAZURE_TRUSTED_SIGNING_ACCOUNT_NAMEAZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAMEAZURE_TRUSTED_SIGNING_PUBLISHER_NAME
Checklist:
- Create Azure Trusted Signing account and certificate profile.
- Record ATS values:
- Endpoint
- Account name
- Certificate profile name
- Publisher name
- Create/choose an Entra app registration (service principal).
- Grant service principal permissions required by Trusted Signing.
- Create a client secret for the service principal.
- Add Azure secrets listed above in GitHub Actions secrets.
- Re-run a tag release and confirm Windows installer is signed.
- Pick the latest nightly and verify it: run the smoke test above against its artifacts and check the nightly channel for regressions.
- Dispatch the Release workflow with
channel=stable. Leaveversionempty unless the version should differ from the one the nightly previewed. - Confirm the
Resolve release commitnotice names the nightly tag and commit you verified. If a newer nightly published in between, the run builds that one instead. - Verify workflow steps:
- preflight passes
- release quality checks pass
build_bundleand all platform builds passpublish_clipublishes the exact release version before the release job- release job uploads expected files
- Smoke test downloaded artifacts.
- macOS build unsigned when expected signed:
- Check all Apple secrets plus
APPLE_TEAM_IDare populated and non-empty. - Confirm the provisioning profile belongs to
APPLE_TEAM_ID.com.t3tools.t3codeand includes Associated Domains.
- Check all Apple secrets plus
- Windows build unsigned when expected signed:
- Check all Azure ATS and auth secrets are populated and non-empty.
- Build fails with signing error:
- Retry with secrets removed to confirm unsigned path still works.
- Re-check certificate/profile names and tenant/client credentials.