A GitHub Actions → Azure CI/CD pipeline, from scratch
az . Let me translate this article now, preserving all code and technical identifiers exactly.
az command by hand. Two YAML files in .github/workflows/ do all the work: one publishes the static front end, the other the .NET API. A git push to main triggers whichever one matches the files that were touched, and only that one.
Two workflows, two triggers
deploy-client.yml and deploy-api.yml share the same shape and nothing more. Each listens to push on main and workflow_dispatch (the manual button in the Actions tab), with a path filter that confines it to its own territory:
Accessing Azure without a stored secret
AZURE_CLIENT_ID , AZURE_TENANT_ID and AZURE_SUBSCRIPTION_ID designate the application and the subscription; on their own, they open nothing. The trust is declared on the Azure side, in a federated credential that only accepts tokens carrying the identity of this repository and its branch. There's no key to rotate or to see leak into a log.
@v3 would follow a mobile tag that an attacker could move out from under us; the SHA locks down exactly which code runs.
The front end: build, then push static files
actions/setup-node on Node 22, with the npm cache keyed on client/package-lock.json , then npm ci rather than npm install . ci installs exactly what the lockfile describes, without ever rewriting it; two runs a month apart produce the same dependency tree.
npm run build:ssg , which first derives read times from the actual word count, runs the production build (Angular prerenders every route to static HTML), generates the sitemap and robots files, then runs a guard: check-prerender.mjs fails if an article page has lost its JSON-LD or its rendered Markdown body.
dist/super-dev-portfolio/browser is ready, it needs to be pushed to the Static Web App. The deployment token isn't stored either: it's fetched at runtime, via the OIDC connection already established.
swa-sd-web in rg-infra-web ); the workflow doesn't create any resource, it deploys into infrastructure that already exists. The last step, placed after the deployment so the key file is live by the time search engines validate it, is an IndexNow ping that notifies Bing and the engines that follow the protocol. It's non-blocking by choice, since a failed crawl notification should never fail an already successful deployment.
The API: test before publishing
dotnet test on the whole solution runs before dotnet publish . A failing test stops right there, before publication. The binary (a .NET 10 isolated worker) then ships as a zip-deploy to afu-sd-api , a Function App on the Flex Consumption plan, via the official Azure/functions-action .
The guardrails
main branch is protected: code arrives via pull request, never a direct push. The path filter bounds each workflow's blast radius. The build fails before deployment, never after.
api , while the front end has none. An Environment is where protection rules are hung (mandatory review, wait timer, reserved secrets) before a job can deploy into it. Putting the API behind one and leaving the front end without translates a deliberate risk asymmetry.
push legible.
The pipeline fits in two short files, with no stored secret and no manual step. That's enough for a push on a Tuesday afternoon to go to production without ceremony.