|
| 1 | +# Token/Placeholder Substitution |
| 2 | + |
| 3 | +Token substitution lets you store secrets and environment-specific values in your pipeline's secret store rather than in your configuration YAML files. The publish pipeline replaces `{#[TOKEN_NAME]#}` placeholders in `configuration.<env>.yaml` with the actual values before running `apiops publish`. |
| 4 | + |
| 5 | +This feature is compatible with APIOps Toolkit configuration files — users migrating from APIOps Toolkit can use their existing configuration files without modification. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## How It Works |
| 10 | + |
| 11 | +1. **Define placeholders** in your `configuration.<env>.yaml` using the `{#[TOKEN_NAME]#}` syntax: |
| 12 | + |
| 13 | + ```yaml |
| 14 | + namedValues: |
| 15 | + - name: my-api-secret |
| 16 | + properties: |
| 17 | + displayName: my-api-secret |
| 18 | + secret: true |
| 19 | + value: "{#[PROD_SECRET_VALUE]#}" |
| 20 | + - name: backend-url |
| 21 | + properties: |
| 22 | + displayName: backend-url |
| 23 | + value: "{#[BACKEND_API_URL]#}" |
| 24 | + ``` |
| 25 | +
|
| 26 | +2. **Store actual values** in your pipeline's secret store (GitHub Actions Secrets or Azure DevOps variable groups / Key Vault). |
| 27 | +
|
| 28 | +3. **Token substitution runs automatically** as a pipeline step before `apiops publish`. Placeholders are replaced with the actual values in memory — the files on disk are modified only within the pipeline run and the secrets are never committed to the repository. |
| 29 | + |
| 30 | +The substitution follows this pattern: |
| 31 | + |
| 32 | +``` |
| 33 | +{#[TOKEN_NAME]#} → value of environment variable TOKEN_NAME |
| 34 | +``` |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## GitHub Actions Setup |
| 39 | + |
| 40 | +### Generated Step |
| 41 | + |
| 42 | +`apiops init` generates a substitution step in each environment's publish job: |
| 43 | + |
| 44 | +```yaml |
| 45 | +- name: Substitute tokens in configuration.prod.yaml |
| 46 | + uses: cschleiden/replace-tokens@v1.3 |
| 47 | + with: |
| 48 | + tokenPrefix: '{#[' |
| 49 | + tokenSuffix: ']#}' |
| 50 | + files: '["configuration.prod.yaml"]' |
| 51 | + env: |
| 52 | + # Map pipeline secrets/variables to environment variables so that |
| 53 | + # {#[TOKEN_NAME]#} placeholders in configuration.prod.yaml are replaced |
| 54 | + # with their actual values before the publish step runs. Example: |
| 55 | + # MY_SECRET: ${{ secrets.MY_SECRET }} |
| 56 | +``` |
| 57 | + |
| 58 | +### Mapping Secrets to Tokens |
| 59 | + |
| 60 | +To substitute a token, add the corresponding secret to your GitHub environment and map it in the `env` block: |
| 61 | + |
| 62 | +```yaml |
| 63 | +- name: Substitute tokens in configuration.prod.yaml |
| 64 | + uses: cschleiden/replace-tokens@v1.3 |
| 65 | + with: |
| 66 | + tokenPrefix: '{#[' |
| 67 | + tokenSuffix: ']#}' |
| 68 | + files: '["configuration.prod.yaml"]' |
| 69 | + env: |
| 70 | + PROD_SECRET_VALUE: ${{ secrets.PROD_SECRET_VALUE }} |
| 71 | + BACKEND_API_URL: ${{ secrets.BACKEND_API_URL }} |
| 72 | +``` |
| 73 | + |
| 74 | +> **Important:** The environment variable name must exactly match the token name inside `{#[...]#}`. Token names are case-sensitive. |
| 75 | + |
| 76 | +### Step-by-Step for GitHub Actions |
| 77 | + |
| 78 | +1. **Add secrets to your GitHub environment:** |
| 79 | + - Go to **Settings → Environments → prod → Add secret** |
| 80 | + - Add a secret for each token (e.g., `PROD_SECRET_VALUE`) |
| 81 | + |
| 82 | +2. **Map secrets to env vars** in the substitution step's `env:` block as shown above. |
| 83 | + |
| 84 | +3. **Define placeholders** in `configuration.prod.yaml` using the matching token names. |
| 85 | + |
| 86 | +4. The substitution step runs during the publish job and replaces the placeholders before `apiops publish` is called. |
| 87 | + |
| 88 | +### Example |
| 89 | + |
| 90 | +`configuration.prod.yaml`: |
| 91 | +```yaml |
| 92 | +namedValues: |
| 93 | + - name: payment-api-key |
| 94 | + properties: |
| 95 | + displayName: payment-api-key |
| 96 | + secret: true |
| 97 | + value: "{#[PAYMENT_API_KEY]#}" |
| 98 | +``` |
| 99 | + |
| 100 | +GitHub environment secret: `PAYMENT_API_KEY = sk-live-abc123...` |
| 101 | + |
| 102 | +Workflow `env:` mapping: |
| 103 | +```yaml |
| 104 | +env: |
| 105 | + PAYMENT_API_KEY: ${{ secrets.PAYMENT_API_KEY }} |
| 106 | +``` |
| 107 | + |
| 108 | +Result: the placeholder `{#[PAYMENT_API_KEY]#}` is replaced with `sk-live-abc123...` before publish. |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## Azure DevOps Setup |
| 113 | + |
| 114 | +### Generated Step |
| 115 | + |
| 116 | +`apiops init` generates a substitution step in each environment's deployment job using the [Replace Tokens](https://marketplace.visualstudio.com/items?itemName=qetza.replacetokens) extension: |
| 117 | + |
| 118 | +```yaml |
| 119 | +- task: replacetokens@6 |
| 120 | + displayName: 'Substitute tokens in configuration.prod.yaml' |
| 121 | + inputs: |
| 122 | + sources: 'configuration.prod.yaml' |
| 123 | + tokenPrefix: '{#[' |
| 124 | + tokenSuffix: ']#}' |
| 125 | +``` |
| 126 | + |
| 127 | +> **Prerequisite:** The [Replace Tokens extension](https://marketplace.visualstudio.com/items?itemName=qetza.replacetokens) (by Guillaume Rouchon / qetza) must be installed in your Azure DevOps organization from the Visual Studio Marketplace. |
| 128 | + |
| 129 | +### Mapping Variables to Tokens |
| 130 | + |
| 131 | +The `replacetokens` task automatically reads from pipeline variables (including those from variable groups). Add your secret values as variables in the `apim-<env>` variable group: |
| 132 | + |
| 133 | +1. Go to **Pipelines → Library → apim-prod** |
| 134 | +2. Add a variable for each token (e.g., `PROD_SECRET_VALUE`) |
| 135 | +3. Check **"Keep this value secret"** to mark it as a secret variable |
| 136 | + |
| 137 | +The substitution task will automatically replace `{#[PROD_SECRET_VALUE]#}` with the variable value from the group. |
| 138 | + |
| 139 | +### Step-by-Step for Azure DevOps |
| 140 | + |
| 141 | +1. **Install the Replace Tokens extension** in your Azure DevOps organization if not already present. |
| 142 | + |
| 143 | +2. **Add secret variables to your variable group:** |
| 144 | + - Go to **Pipelines → Library → apim-prod** |
| 145 | + - Add each token as a secret variable (e.g., `PROD_SECRET_VALUE`) |
| 146 | + |
| 147 | +3. **Define placeholders** in `configuration.prod.yaml` using matching variable names. |
| 148 | + |
| 149 | +4. The substitution step runs automatically before the publish task. |
| 150 | + |
| 151 | +### Example |
| 152 | + |
| 153 | +`configuration.prod.yaml`: |
| 154 | +```yaml |
| 155 | +backends: |
| 156 | + - name: order-service |
| 157 | + properties: |
| 158 | + url: "{#[ORDER_SERVICE_URL]#}" |
| 159 | + description: Order processing backend |
| 160 | +``` |
| 161 | + |
| 162 | +Variable group `apim-prod`: |
| 163 | +| Variable | Value | Secret | |
| 164 | +|----------|-------|--------| |
| 165 | +| `ORDER_SERVICE_URL` | `https://orders.contoso.com/api` | ✓ | |
| 166 | + |
| 167 | +Result: `{#[ORDER_SERVICE_URL]#}` is replaced with `https://orders.contoso.com/api` before publish. |
| 168 | + |
| 169 | +--- |
| 170 | + |
| 171 | +## Migration from APIOps Toolkit |
| 172 | + |
| 173 | +If you are migrating from APIOps Toolkit, your existing `configuration.<env>.yaml` files that use `{#[TOKEN_NAME]#}` placeholders work without modification. The same syntax is supported. |
| 174 | + |
| 175 | +The only difference is where secrets are stored and mapped: |
| 176 | + |
| 177 | +| | APIOps Toolkit | APIOps CLI | |
| 178 | +|---|---|---| |
| 179 | +| **Token syntax** | `{#[TOKEN_NAME]#}` | `{#[TOKEN_NAME]#}` (identical) | |
| 180 | +| **GitHub Actions** | `cschleiden/replace-tokens@v1.3` | `cschleiden/replace-tokens@v1.3` (same action) | |
| 181 | +| **Azure DevOps** | `qetza.replacetokens@6` | `replacetokens@6` (same extension) | |
| 182 | +| **Token prefix/suffix** | `{#[` / `]#}` | `{#[` / `]#}` (identical) | |
| 183 | + |
| 184 | +### Migration Steps |
| 185 | + |
| 186 | +1. Copy your existing `configuration.<env>.yaml` files to your new repository — no changes required. |
| 187 | + |
| 188 | +2. Run `apiops init` to generate the pipeline scaffolding. The publish pipeline includes token substitution steps out of the box. |
| 189 | + |
| 190 | +3. Re-create your secrets in the new pipeline: |
| 191 | + - **GitHub Actions**: Add each secret to the corresponding GitHub environment. |
| 192 | + - **Azure DevOps**: Add each secret to the corresponding `apim-<env>` variable group. |
| 193 | + |
| 194 | +4. For GitHub Actions, add the `env:` mappings to the substitution step as described in [GitHub Actions Setup](#github-actions-setup). |
| 195 | + |
| 196 | +--- |
| 197 | + |
| 198 | +## Common Use Cases |
| 199 | + |
| 200 | +### Named Value Secrets |
| 201 | + |
| 202 | +```yaml |
| 203 | +namedValues: |
| 204 | + - name: subscription-key |
| 205 | + properties: |
| 206 | + displayName: subscription-key |
| 207 | + secret: true |
| 208 | + value: "{#[SUBSCRIPTION_KEY]#}" |
| 209 | +``` |
| 210 | + |
| 211 | +### Backend URLs |
| 212 | + |
| 213 | +```yaml |
| 214 | +backends: |
| 215 | + - name: inventory-api |
| 216 | + properties: |
| 217 | + url: "{#[INVENTORY_API_BASE_URL]#}" |
| 218 | +``` |
| 219 | + |
| 220 | +### Multiple Tokens in One File |
| 221 | + |
| 222 | +```yaml |
| 223 | +namedValues: |
| 224 | + - name: db-connection |
| 225 | + properties: |
| 226 | + value: "{#[DB_CONNECTION_STRING]#}" |
| 227 | + - name: auth-secret |
| 228 | + properties: |
| 229 | + secret: true |
| 230 | + value: "{#[AUTH_SECRET_KEY]#}" |
| 231 | +backends: |
| 232 | + - name: payment-service |
| 233 | + properties: |
| 234 | + url: "{#[PAYMENT_SERVICE_URL]#}" |
| 235 | +``` |
| 236 | + |
| 237 | +### Using Different Values per Environment |
| 238 | + |
| 239 | +Use separate `configuration.<env>.yaml` files (e.g., `configuration.dev.yaml`, `configuration.prod.yaml`) each referencing the same token names. The pipeline substitutes values from the environment-specific secret store, so `{#[API_KEY]#}` resolves to the dev key in dev and the prod key in prod. |
| 240 | + |
| 241 | +--- |
| 242 | + |
| 243 | +## Security Notes |
| 244 | + |
| 245 | +- Tokens are replaced **in the pipeline runner's memory** — they are never committed to the repository. |
| 246 | +- Use your pipeline platform's secret storage (GitHub Actions Secrets or Azure DevOps secret variables / Key Vault) — never store actual secret values in YAML files. |
| 247 | +- The replaced configuration YAML files are only visible within the single pipeline run and are discarded after the run completes. |
| 248 | + |
| 249 | +--- |
| 250 | + |
| 251 | +## Related |
| 252 | + |
| 253 | +- [Environment Overrides](environment-overrides.md) — merge environment-specific configuration before publishing |
| 254 | +- [GitHub Actions Integration](../ci-cd/github-actions.md) — full GitHub Actions pipeline guide |
| 255 | +- [Azure DevOps Integration](../ci-cd/azure-devops.md) — full Azure DevOps pipeline guide |
| 256 | +- [apiops init](../commands/init.md) — generate pipeline scaffolding |
0 commit comments