Skip to content

Commit bab1c32

Browse files
authored
Merge pull request #127 from Azure/copilot/enable-pipeline-token-substitution
feat: add {#[TOKEN_NAME]#} substitution step to publish pipelines (APIOps Toolkit parity)
2 parents 930f92c + 83a49e2 commit bab1c32

24 files changed

Lines changed: 1044 additions & 251 deletions

.devcontainer/devcontainer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"remoteEnv": {
1212
"PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/node_modules/.bin"
1313
},
14-
"postCreateCommand": "npm ci --ignore-scripts",
14+
"postCreateCommand": "npm ci --ignore-scripts && WELCOME_SOURCE=\"source \\\"$PWD/.devcontainer/welcome.sh\\\"\" && (grep -qxF \"$WELCOME_SOURCE\" ~/.bashrc || echo \"$WELCOME_SOURCE\" >> ~/.bashrc) && mkdir -p ~/.config/vscode-dev-containers && touch ~/.config/vscode-dev-containers/first-run-notice-already-displayed",
1515
"customizations": {
1616
"vscode": {
1717
"settings": {
@@ -22,7 +22,9 @@
2222
"eslint.format.enable": true,
2323
"eslint.validate": ["typescript", "typescriptreact"],
2424
"js/ts.tsdk.path": "node_modules/typescript/lib",
25-
"js/ts.tsdk.promptToUseWorkspaceVersion": true
25+
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
26+
"extensions.ignoreRecommendations": true,
27+
"extensions.showRecommendationsOnInstall": false
2628
},
2729
"extensions": [
2830
"dbaeumer.vscode-eslint",

.devcontainer/welcome.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
3+
# Only print the banner in interactive terminals.
4+
if [[ $- != *i* ]]; then
5+
return
6+
fi
7+
8+
WELCOME_MARKER_FILE="$HOME/.config/apiops-cli/welcome-shown"
9+
10+
# Print only once per container/user lifecycle.
11+
if [[ -f "$WELCOME_MARKER_FILE" ]]; then
12+
return
13+
fi
14+
15+
mkdir -p "$HOME/.config/apiops-cli"
16+
touch "$WELCOME_MARKER_FILE"
17+
18+
printf "👋 Welcome to apiops-cli!\n\n"
19+
printf "📚 To get started, run:\n"
20+
printf " npm ci && npm run build\n\n"

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Requires Node.js 22 or later.
4242
- **Incremental publish** — Deploy only changed resources via git diff
4343
- **Dry-run mode** — Preview changes before applying them
4444
- **CI/CD scaffolding**`apiops init` generates GitHub Actions or Azure DevOps pipelines
45+
- **Token substitution** — Replace `{#[TOKEN_NAME]#}` placeholders in config files with pipeline secrets before publish
4546
- **Multiple auth methods** — Azure CLI, managed identity, workload identity (OIDC), service principal
4647

4748
## Documentation Structure
@@ -64,6 +65,7 @@ docs/
6465
│ ├── multi-environment.md — Dev / staging / prod promotion
6566
│ ├── multi-team-workflows.md — Selective extraction, CODEOWNERS
6667
│ ├── code-first-workflow.md — IDE → git → CI/CD → APIM
68+
│ ├── token-substitution.md — Pipeline token/placeholder substitution
6769
│ └── migration-from-v1.md — Migrate from Azure/apiops toolkit
6870
├── ci-cd/
6971
│ ├── github-actions.md — GitHub Actions integration

docs/ci-cd/authentication-patterns.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ variables:
146146
- group: apim-config # Contains APIM_RESOURCE_GROUP, APIM_SERVICE_NAME, etc.
147147

148148
steps:
149-
- task: NodeTool@0
149+
- task: UseNode@1
150150
inputs:
151151
versionSpec: '22.x'
152152

docs/ci-cd/azure-devops.md

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ flowchart TD
6565
J --> L[Log: create a pull request]
6666
```
6767

68-
1. **Node.js setup** — Installs Node.js 22.x via `NodeTool@0`
68+
1. **Node.js setup** — Installs Node.js 22.x via `UseNode@1`
6969
2. **Install dependencies** — Runs `npm ci`
7070
3. **Run extract** — Executes `apiops extract` via `AzureCLI@2` task, authenticating through the service connection
7171
4. **Publish artifacts** — Uploads the artifact directory as a pipeline artifact named `apim-artifacts`
@@ -127,24 +127,26 @@ The pipeline runs automatically when changes to artifact files or configuration
127127
| Parameter | Type | Default | Description |
128128
|-----------|------|---------|-------------|
129129
| `COMMIT_ID_CHOICE` | string | `publish-artifacts-in-last-commit` | Choose `publish-artifacts-in-last-commit` for incremental publish, or `publish-all-artifacts-in-repo` for a full publish |
130-
| `ENVIRONMENT` | string | `all` | Which environment to publish to: `all`, `dev`, or `prod` |
130+
| `ENVIRONMENT` | string | `dev` | Which environment to publish to (for example `dev` or `prod`) |
131131

132132
### Multi-Stage Deployment
133133

134-
The pipeline generates one stage per environment. Stages run sequentially — each stage depends on the previous:
134+
The pipeline generates one stage per environment. The selected stage runs based on the `ENVIRONMENT` parameter.
135135

136136
```mermaid
137137
flowchart LR
138-
A[Publish_dev] --> B[Publish_prod]
138+
A[ENVIRONMENT=dev] --> B[Publish_dev]
139+
C[ENVIRONMENT=prod] --> D[Publish_prod]
139140
```
140141

141142
Each stage:
142143

143-
1. **Conditionally runs** — Only executes if the `ENVIRONMENT` parameter matches the stage name or is `all`
144+
1. **Conditionally runs** — Only executes when the `ENVIRONMENT` parameter matches the stage name
144145
2. **Uses a deployment job** — Wraps the publish step in a `deployment` job targeting an [Azure DevOps environment](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/environments) for approval gates
145146
3. **Loads per-environment variables** — Each stage uses its own variable group (`apim-dev`, `apim-prod`)
146147
4. **Authenticates per-environment** — Uses environment-specific service connections (`AZURE_SERVICE_CONNECTION_DEV`, `AZURE_SERVICE_CONNECTION_PROD`)
147-
5. **Applies overrides** — Passes `--override configuration.{env}.yaml` to apply [environment-specific overrides](../guides/environment-overrides.md)
148+
5. **Substitutes tokens** — Replaces `{#[TOKEN_NAME]#}` placeholders in `configuration.<env>.yaml` with secret variable values before publishing
149+
6. **Applies overrides** — Passes `--overrides configuration.{env}.yaml` to apply [environment-specific overrides](../guides/environment-overrides.md)
148150

149151
### Publish Pipeline Walkthrough
150152

@@ -155,15 +157,15 @@ For incremental publish (default), `--commit-id $(Build.SourceVersion)` is passe
155157
displayName: 'Publish to dev (incremental - last commit only)'
156158
condition: ne('${{ parameters.COMMIT_ID_CHOICE }}', 'publish-all-artifacts-in-repo')
157159
inputs:
158-
azureSubscription: '$(AZURE_SERVICE_CONNECTION_DEV)'
160+
azureSubscription: 'AZURE_SERVICE_CONNECTION_DEV'
159161
scriptType: 'bash'
160162
scriptLocation: 'inlineScript'
161163
inlineScript: |
162164
npx apiops publish \
163165
--resource-group $(APIM_RESOURCE_GROUP_DEV) \
164166
--service-name $(APIM_SERVICE_NAME_DEV) \
165167
--source ./apim-artifacts \
166-
--override configuration.dev.yaml \
168+
--overrides configuration.dev.yaml \
167169
--commit-id $(Build.SourceVersion) \
168170
--subscription-id $(AZURE_SUBSCRIPTION_ID)
169171
```
@@ -293,6 +295,35 @@ In your `package.json`, pin to a specific version:
293295
}
294296
```
295297

298+
### Using Token Substitution
299+
300+
To replace `{#[TOKEN_NAME]#}` placeholders in `configuration.<env>.yaml` with secret variable values:
301+
302+
1. **Install the [Replace Tokens extension](https://marketplace.visualstudio.com/items?itemName=qetza.replacetokens)** in your Azure DevOps organization (if not already installed).
303+
304+
You can do this via CLI:
305+
```bash
306+
az devops extension install --publisher-id qetza --extension-id replacetokens
307+
```
308+
309+
2. **Add secret variables** to the `apim-<env>` variable group. See the Azure DevOps documentation for [adding variables to a variable group](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups) and [marking variables as secret](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/set-secret-variables).
310+
311+
For example, to substitute `{#[BACKEND_URL]#}` in your configuration file:
312+
313+
`configuration.prod.yaml`:
314+
```yaml
315+
backends:
316+
- name: my-backend
317+
properties:
318+
url: "{#[BACKEND_URL]#}"
319+
```
320+
321+
Add `BACKEND_URL` as a secret variable in the `apim-prod` variable group with the actual backend URL as the value.
322+
323+
3. The substitution step runs automatically before publish.
324+
325+
See the [Token Substitution Guide](../guides/token-substitution.md) for full details, including migration from APIOps Toolkit.
326+
296327
---
297328

298329
## Troubleshooting
@@ -301,20 +332,22 @@ In your `package.json`, pin to a specific version:
301332
|---------|-------|-----|
302333
| `AzureCLI@2` fails with "service connection not found" | Variable group not linked or variable name mismatch | Verify the variable group is linked to the pipeline and `AZURE_SERVICE_CONNECTION` is defined |
303334
| Extract shows "No changes to commit" | APIM config hasn't changed since last extract | Expected behavior — no branch is created |
304-
| Publish stage is skipped | `ENVIRONMENT` parameter doesn't match the stage | Set `ENVIRONMENT` to `all` or the specific stage name |
335+
| Publish stage is skipped | `ENVIRONMENT` parameter doesn't match the stage | Set `ENVIRONMENT` to the specific stage name (for example `dev` or `prod`) |
305336
| `npm ci` fails | `package.json` or `package-lock.json` missing | Run `apiops init` to generate project files, then commit them |
306337
| "publish-all-artifacts-in-repo" deploys everything | Expected — this mode publishes all artifacts, ignoring git diff | Use `publish-artifacts-in-last-commit` (default) for incremental |
307338
| Approval gate blocks deployment | Environment checks configured | Approve in **Pipelines → Environments → {env}** |
339+
| Run is stuck with "This pipeline needs permission to access a resource" | Environment resource isn't authorized for pipeline use | Authorize the environment in Azure DevOps or run the prompt step that PATCHes `pipelinePermissions/environment/{id}` with `{"allPipelines":{"authorized":true}}` |
308340
| `--subscription-id` error | `AZURE_SUBSCRIPTION_ID` not set in variable group | Add it to the relevant variable group |
309341

310342
---
311343

312-
## Related
344+
## Further Reading
313345

314346
- [GitHub Actions Integration](github-actions.md) — alternative CI/CD platform
315347
- [apiops init](../commands/init.md) — generates pipeline files
316348
- [apiops extract](../commands/extract.md) — extract command reference
317349
- [apiops publish](../commands/publish.md) — publish command reference
318350
- [Authentication Guide](../guides/authentication.md) — auth methods and RBAC
319351
- [Environment Overrides](../guides/environment-overrides.md) — per-environment configuration
352+
- [Token Substitution](../guides/token-substitution.md) — pipeline placeholder substitution with `{#[TOKEN_NAME]#}`
320353
- [Filtering Resources](../guides/filtering-resources.md) — extract specific APIs

docs/ci-cd/github-actions.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ The workflow runs automatically when changes are pushed to `main` in these paths
100100
1. **Resolves the commit ID** — captures `GITHUB_SHA` for incremental publish.
101101
2. **Checks out the repository** with `fetch-depth: 2` (needed for git diff).
102102
3. **Authenticates with Azure** using OIDC federated credentials.
103-
4. **Runs `apiops publish`** in one of two modes:
103+
4. **Substitutes tokens** — replaces `{#[TOKEN_NAME]#}` placeholders in `configuration.<env>.yaml` with pipeline secret values.
104+
5. **Runs `apiops publish`** in one of two modes:
104105
- **Incremental** (default): uses `--commit-id` to publish only changed files.
105106
- **Full**: publishes all artifacts in the repository (useful for recovery or initial setup).
106107

@@ -212,6 +213,24 @@ Use GitHub environment protection rules for production deployments:
212213

213214
The publish workflow will pause and wait for approval before deploying to prod.
214215

216+
### Using Token Substitution
217+
218+
To replace `{#[TOKEN_NAME]#}` placeholders in your configuration YAML with pipeline secrets, add the secret mappings to the `env:` block of the generated substitution step:
219+
220+
```yaml
221+
- name: Substitute tokens in configuration.prod.yaml
222+
uses: cschleiden/replace-tokens@v1.3
223+
with:
224+
tokenPrefix: '{#['
225+
tokenSuffix: ']#}'
226+
files: '["configuration.prod.yaml"]'
227+
env:
228+
MY_SECRET: ${{ secrets.MY_SECRET }}
229+
BACKEND_URL: ${{ secrets.BACKEND_URL }}
230+
```
231+
232+
See the [Token Substitution Guide](../guides/token-substitution.md) for full details, including migration from APIOps Toolkit.
233+
215234
### Adding Environment Overrides
216235

217236
To use [environment-specific overrides](../guides/environment-overrides.md), add the `--overrides` flag to the publish step in the workflow:
@@ -271,4 +290,5 @@ For authentication issues, see the [Authentication Guide](../guides/authenticati
271290

272291
- [Authentication Guide](../guides/authentication.md) — all auth methods and RBAC roles
273292
- [Environment Overrides](../guides/environment-overrides.md) — per-environment configuration
293+
- [Token Substitution](../guides/token-substitution.md) — pipeline placeholder substitution with `{#[TOKEN_NAME]#}`
274294
- [Scenarios and Workflows](../guides/scenarios-and-workflows.md) — portal-first vs. code-first patterns

0 commit comments

Comments
 (0)