@egulatee/pulumi-stack-alias 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +21 -0
- package/.github/workflows/ci.yml +63 -0
- package/.github/workflows/publish.yml +35 -0
- package/LICENSE +21 -0
- package/README.md +496 -0
- package/dist/alias.d.ts +104 -0
- package/dist/alias.d.ts.map +1 -0
- package/dist/alias.js +215 -0
- package/dist/alias.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/examples/README.md +67 -0
- package/examples/conditional-alias/Pulumi.dev.yaml +2 -0
- package/examples/conditional-alias/Pulumi.shared.yaml +2 -0
- package/examples/conditional-alias/Pulumi.staging.yaml +2 -0
- package/examples/conditional-alias/Pulumi.yaml +4 -0
- package/examples/conditional-alias/index.ts +30 -0
- package/examples/simple-alias/Pulumi.dev.yaml +4 -0
- package/examples/simple-alias/Pulumi.yaml +4 -0
- package/examples/simple-alias/index.ts +23 -0
- package/package.json +47 -0
- package/src/alias.ts +204 -0
- package/src/index.ts +33 -0
- package/src/types.ts +77 -0
- package/tests/alias.test.ts +463 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +19 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"parser": "@typescript-eslint/parser",
|
|
3
|
+
"extends": [
|
|
4
|
+
"eslint:recommended",
|
|
5
|
+
"plugin:@typescript-eslint/recommended"
|
|
6
|
+
],
|
|
7
|
+
"plugins": ["@typescript-eslint"],
|
|
8
|
+
"env": {
|
|
9
|
+
"node": true,
|
|
10
|
+
"es6": true
|
|
11
|
+
},
|
|
12
|
+
"parserOptions": {
|
|
13
|
+
"ecmaVersion": 2020,
|
|
14
|
+
"sourceType": "module"
|
|
15
|
+
},
|
|
16
|
+
"rules": {
|
|
17
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
18
|
+
"@typescript-eslint/explicit-function-return-type": "off",
|
|
19
|
+
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Test
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: "20"
|
|
22
|
+
cache: "npm"
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: npm ci
|
|
26
|
+
|
|
27
|
+
- name: Run linter
|
|
28
|
+
run: npm run lint
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: npm test
|
|
32
|
+
|
|
33
|
+
- name: Run tests with coverage
|
|
34
|
+
run: npm run test:coverage
|
|
35
|
+
|
|
36
|
+
- name: Upload coverage
|
|
37
|
+
uses: codecov/codecov-action@v3
|
|
38
|
+
with:
|
|
39
|
+
files: ./coverage/coverage-final.json
|
|
40
|
+
|
|
41
|
+
build:
|
|
42
|
+
name: Build
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- uses: actions/setup-node@v4
|
|
48
|
+
with:
|
|
49
|
+
node-version: "20"
|
|
50
|
+
cache: "npm"
|
|
51
|
+
|
|
52
|
+
- name: Install dependencies
|
|
53
|
+
run: npm ci
|
|
54
|
+
|
|
55
|
+
- name: Build TypeScript
|
|
56
|
+
run: npm run build
|
|
57
|
+
|
|
58
|
+
- name: Check build output
|
|
59
|
+
run: |
|
|
60
|
+
test -f dist/index.js || exit 1
|
|
61
|
+
test -f dist/index.d.ts || exit 1
|
|
62
|
+
test -f dist/alias.js || exit 1
|
|
63
|
+
test -f dist/types.js || exit 1
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
name: Publish to npm
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: "20"
|
|
20
|
+
registry-url: "https://registry.npmjs.org"
|
|
21
|
+
cache: "npm"
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: npm test
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: npm run build
|
|
31
|
+
|
|
32
|
+
- name: Publish to npm
|
|
33
|
+
run: npm publish --access public
|
|
34
|
+
env:
|
|
35
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eric Gulatee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# @egulatee/pulumi-stack-alias
|
|
2
|
+
|
|
3
|
+
Producer-side stack aliasing for Pulumi using lightweight proxy stacks. Consumers use standard `StackReference` (zero library dependency), while producers use this library to create alias stacks that re-export outputs from canonical stacks.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/egulatee/pulumi-stack-alias/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/@egulatee/pulumi-stack-alias)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
|
|
11
|
+
When managing infrastructure across multiple environments (dev, staging, prod), consumer projects need to reference shared infrastructure stacks. Traditional approaches either:
|
|
12
|
+
- Force consumers to know which stack holds the real resources
|
|
13
|
+
- Scatter mapping logic across every consumer project
|
|
14
|
+
- Require complex consumer-side resolvers with library dependencies
|
|
15
|
+
|
|
16
|
+
**The aliasing decision belongs with the producer (infrastructure project), not the consumer.**
|
|
17
|
+
|
|
18
|
+
## Solution: Producer-Controlled Proxy Stacks
|
|
19
|
+
|
|
20
|
+
Alias stacks re-export outputs from canonical stacks. Consumers use standard Pulumi `StackReference` with **no library dependency**. Producers use this library to create lightweight proxy stacks.
|
|
21
|
+
|
|
22
|
+
### How It Works
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
infrastructure/shared → canonical stack, exports real resources
|
|
26
|
+
infrastructure/dev → proxy stack, re-exports outputs from shared
|
|
27
|
+
infrastructure/staging → proxy stack, re-exports outputs from shared
|
|
28
|
+
infrastructure/prod → canonical stack, exports real resources
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Consumer uses standard Pulumi:
|
|
32
|
+
```typescript
|
|
33
|
+
const stack = new pulumi.StackReference(`org/infrastructure/${pulumi.getStack()}`);
|
|
34
|
+
const vpcId = stack.requireOutput("vpcId");
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Zero consumer dependencies!** The consumer has no knowledge of aliasing. When `application/dev` deploys, it reads `infrastructure/dev`, which is a proxy stack that re-exports outputs from `infrastructure/shared`.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
**Producer projects only:**
|
|
42
|
+
```bash
|
|
43
|
+
npm install @egulatee/pulumi-stack-alias
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Consumer projects:** No installation needed! Use standard Pulumi `StackReference`.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Producer Side (Infrastructure Project)
|
|
51
|
+
|
|
52
|
+
Create alias stacks that re-export outputs from canonical stacks:
|
|
53
|
+
|
|
54
|
+
#### Simple Alias
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// infrastructure/index.ts
|
|
58
|
+
import { createStackAlias } from "@egulatee/pulumi-stack-alias";
|
|
59
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
60
|
+
|
|
61
|
+
const config = new pulumi.Config();
|
|
62
|
+
const aliasTarget = config.get("aliasTarget");
|
|
63
|
+
|
|
64
|
+
if (aliasTarget) {
|
|
65
|
+
// This is an alias stack — re-export outputs from target
|
|
66
|
+
const alias = createStackAlias({
|
|
67
|
+
targetProject: "infrastructure",
|
|
68
|
+
targetStack: aliasTarget,
|
|
69
|
+
outputs: ["vpcId", "endpoint", "clusterName"],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const vpcId = alias.vpcId;
|
|
73
|
+
export const endpoint = alias.endpoint;
|
|
74
|
+
export const clusterName = alias.clusterName;
|
|
75
|
+
} else {
|
|
76
|
+
// This is a canonical stack — create actual resources
|
|
77
|
+
const vpc = new aws.ec2.Vpc("main", {
|
|
78
|
+
cidrBlock: "10.0.0.0/16",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const vpcId = vpc.id;
|
|
82
|
+
export const endpoint = pulumi.output("https://api.example.com");
|
|
83
|
+
export const clusterName = pulumi.output("my-cluster");
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Configure your stack files:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
# infrastructure/Pulumi.shared.yaml
|
|
91
|
+
config:
|
|
92
|
+
# No aliasTarget — this is the canonical stack
|
|
93
|
+
|
|
94
|
+
# infrastructure/Pulumi.dev.yaml
|
|
95
|
+
config:
|
|
96
|
+
infrastructure:aliasTarget: shared
|
|
97
|
+
|
|
98
|
+
# infrastructure/Pulumi.staging.yaml
|
|
99
|
+
config:
|
|
100
|
+
infrastructure:aliasTarget: shared
|
|
101
|
+
|
|
102
|
+
# infrastructure/Pulumi.prod.yaml
|
|
103
|
+
config:
|
|
104
|
+
# No aliasTarget — this is a canonical stack (separate from shared)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Conditional Alias (Pattern-Based)
|
|
108
|
+
|
|
109
|
+
For automatic aliasing without config:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// infrastructure/index.ts
|
|
113
|
+
import { createConditionalAlias } from "@egulatee/pulumi-stack-alias";
|
|
114
|
+
|
|
115
|
+
const alias = createConditionalAlias({
|
|
116
|
+
targetProject: "infrastructure",
|
|
117
|
+
patterns: [
|
|
118
|
+
{ pattern: "*/prod", target: "prod" },
|
|
119
|
+
{ pattern: "*/staging", target: "shared" },
|
|
120
|
+
{ pattern: "*/dev", target: "shared" },
|
|
121
|
+
{ pattern: "*/*-ephemeral", target: "shared" },
|
|
122
|
+
],
|
|
123
|
+
defaultTarget: "shared",
|
|
124
|
+
outputs: ["vpcId", "endpoint", "clusterName"],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const vpcId = alias.vpcId;
|
|
128
|
+
export const endpoint = alias.endpoint;
|
|
129
|
+
export const clusterName = alias.clusterName;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Simple API
|
|
133
|
+
|
|
134
|
+
Simplified API for common cases:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { createSimpleAlias } from "@egulatee/pulumi-stack-alias";
|
|
138
|
+
|
|
139
|
+
const alias = createSimpleAlias("infrastructure", "shared", ["vpcId"]);
|
|
140
|
+
export const vpcId = alias.vpcId;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Consumer Side (Application Project)
|
|
144
|
+
|
|
145
|
+
Use standard Pulumi `StackReference` — **no library dependency**:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// application/index.ts
|
|
149
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
150
|
+
import * as aws from "@pulumi/aws";
|
|
151
|
+
|
|
152
|
+
// Standard Pulumi StackReference — no special library needed!
|
|
153
|
+
const infraStack = new pulumi.StackReference(
|
|
154
|
+
`${pulumi.getOrganization()}/infrastructure/${pulumi.getStack()}`
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const vpcId = infraStack.requireOutput("vpcId");
|
|
158
|
+
const endpoint = infraStack.requireOutput("endpoint");
|
|
159
|
+
|
|
160
|
+
const subnet = new aws.ec2.Subnet("app-subnet", {
|
|
161
|
+
vpcId: vpcId,
|
|
162
|
+
cidrBlock: "10.0.1.0/24",
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The consumer has **zero knowledge** of aliasing. When `application/dev` deploys:
|
|
167
|
+
1. Reads `infrastructure/dev` (a proxy stack)
|
|
168
|
+
2. Gets outputs (re-exported from `infrastructure/shared`)
|
|
169
|
+
3. No library dependency required!
|
|
170
|
+
|
|
171
|
+
## Deployment Flow
|
|
172
|
+
|
|
173
|
+
### Initial Setup
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Deploy canonical stacks (creates real resources)
|
|
177
|
+
pulumi up --stack shared
|
|
178
|
+
pulumi up --stack prod
|
|
179
|
+
|
|
180
|
+
# Deploy alias stacks (creates proxy outputs)
|
|
181
|
+
pulumi up --stack dev
|
|
182
|
+
pulumi up --stack staging
|
|
183
|
+
|
|
184
|
+
# Consumer deployments — no alias awareness needed
|
|
185
|
+
cd application && pulumi up --stack dev
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Keeping Outputs Fresh
|
|
189
|
+
|
|
190
|
+
Alias stacks capture outputs at deploy time. When canonical stack outputs change, redeploy aliases:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Update canonical stack
|
|
194
|
+
pulumi up --stack shared
|
|
195
|
+
|
|
196
|
+
# Sync alias stacks (fast — no real resources)
|
|
197
|
+
pulumi up --stack dev
|
|
198
|
+
pulumi up --stack staging
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### CI/CD Orchestration
|
|
202
|
+
|
|
203
|
+
Automate alias synchronization in CI/CD:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
# .github/workflows/deploy.yml
|
|
207
|
+
jobs:
|
|
208
|
+
deploy-canonical:
|
|
209
|
+
runs-on: ubuntu-latest
|
|
210
|
+
steps:
|
|
211
|
+
- name: Deploy shared stack
|
|
212
|
+
run: pulumi up --stack shared --yes
|
|
213
|
+
|
|
214
|
+
sync-aliases:
|
|
215
|
+
needs: deploy-canonical
|
|
216
|
+
runs-on: ubuntu-latest
|
|
217
|
+
strategy:
|
|
218
|
+
matrix:
|
|
219
|
+
stack: [dev, staging]
|
|
220
|
+
steps:
|
|
221
|
+
- name: Sync alias stack
|
|
222
|
+
run: pulumi up --stack ${{ matrix.stack }} --yes
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Alias deployments are fast (seconds) since they create no real resources — just re-export outputs.
|
|
226
|
+
|
|
227
|
+
## API Reference
|
|
228
|
+
|
|
229
|
+
### `createStackAlias(config)`
|
|
230
|
+
|
|
231
|
+
Creates a stack alias that re-exports outputs from a target stack.
|
|
232
|
+
|
|
233
|
+
**Parameters:**
|
|
234
|
+
- `config.targetProject` (string) - Target project name
|
|
235
|
+
- `config.targetStack` (string) - Target stack name
|
|
236
|
+
- `config.targetOrg` (optional string) - Target organization (defaults to current org)
|
|
237
|
+
- `config.outputs` (string[]) - List of output names to re-export
|
|
238
|
+
|
|
239
|
+
**Returns:** `AliasExports` - Record of Pulumi Outputs
|
|
240
|
+
|
|
241
|
+
**Example:**
|
|
242
|
+
```typescript
|
|
243
|
+
const alias = createStackAlias({
|
|
244
|
+
targetProject: "infrastructure",
|
|
245
|
+
targetStack: "shared",
|
|
246
|
+
outputs: ["vpcId", "endpoint"],
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export const vpcId = alias.vpcId;
|
|
250
|
+
export const endpoint = alias.endpoint;
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### `createConditionalAlias(config)`
|
|
254
|
+
|
|
255
|
+
Creates a conditional alias based on pattern matching.
|
|
256
|
+
|
|
257
|
+
**Parameters:**
|
|
258
|
+
- `config.targetProject` (string) - Target project name
|
|
259
|
+
- `config.patterns` (PatternRule[]) - Pattern matching rules (evaluated in order, first match wins)
|
|
260
|
+
- `config.defaultTarget` (optional string) - Default target if no pattern matches
|
|
261
|
+
- `config.targetOrg` (optional string) - Target organization (defaults to current org)
|
|
262
|
+
- `config.outputs` (string[]) - List of output names to re-export
|
|
263
|
+
|
|
264
|
+
**Returns:** `AliasExports` - Record of Pulumi Outputs
|
|
265
|
+
|
|
266
|
+
**Example:**
|
|
267
|
+
```typescript
|
|
268
|
+
const alias = createConditionalAlias({
|
|
269
|
+
targetProject: "infrastructure",
|
|
270
|
+
patterns: [
|
|
271
|
+
{ pattern: "*/prod", target: "prod" },
|
|
272
|
+
{ pattern: "*/dev", target: "shared" },
|
|
273
|
+
],
|
|
274
|
+
defaultTarget: "shared",
|
|
275
|
+
outputs: ["vpcId"],
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### `createSimpleAlias(targetProject, targetStack, outputs)`
|
|
280
|
+
|
|
281
|
+
Simplified API for creating aliases.
|
|
282
|
+
|
|
283
|
+
**Parameters:**
|
|
284
|
+
- `targetProject` (string) - Target project name
|
|
285
|
+
- `targetStack` (string) - Target stack name
|
|
286
|
+
- `outputs` (string[]) - List of output names to re-export
|
|
287
|
+
|
|
288
|
+
**Returns:** `AliasExports` - Record of Pulumi Outputs
|
|
289
|
+
|
|
290
|
+
**Example:**
|
|
291
|
+
```typescript
|
|
292
|
+
const alias = createSimpleAlias("infrastructure", "shared", ["vpcId"]);
|
|
293
|
+
export const vpcId = alias.vpcId;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### `matchesPattern(pattern, project, stack)`
|
|
297
|
+
|
|
298
|
+
Pattern matching with wildcard support.
|
|
299
|
+
|
|
300
|
+
**Wildcard rules:**
|
|
301
|
+
- `*` matches any value
|
|
302
|
+
- `*-suffix` matches strings ending with `-suffix`
|
|
303
|
+
- `prefix-*` matches strings starting with `prefix-`
|
|
304
|
+
- `exact` matches exactly
|
|
305
|
+
|
|
306
|
+
**Example:**
|
|
307
|
+
```typescript
|
|
308
|
+
import { matchesPattern } from "@egulatee/pulumi-stack-alias";
|
|
309
|
+
|
|
310
|
+
matchesPattern("*/dev", "myproject", "dev") // true
|
|
311
|
+
matchesPattern("*/*-ephemeral", "app", "pr-123-ephemeral") // true
|
|
312
|
+
matchesPattern("app-*/prod-*", "app-api", "prod-us") // true
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Pattern Format
|
|
316
|
+
|
|
317
|
+
Patterns follow the format: `"projectPattern/stackPattern"`
|
|
318
|
+
|
|
319
|
+
Examples:
|
|
320
|
+
- `"*/dev"` - Any project, stack must be "dev"
|
|
321
|
+
- `"myproject/*"` - Project must be "myproject", any stack
|
|
322
|
+
- `"*/*-ephemeral"` - Any project, stack must end with "-ephemeral"
|
|
323
|
+
- `"app-*/prod-*"` - Project must start with "app-", stack must start with "prod-"
|
|
324
|
+
|
|
325
|
+
## Comparison with Other Approaches
|
|
326
|
+
|
|
327
|
+
| Concern | Producer Redirect (v0.1.0) | Producer Proxy (v0.2.0) | Consumer Config |
|
|
328
|
+
|---|---|---|---|
|
|
329
|
+
| Who owns mapping | Producer ✓ | Producer ✓ | Consumer |
|
|
330
|
+
| Consumer library dependency | Yes | **None** ✓ | Varies |
|
|
331
|
+
| Alias stack weight | Trivial (one pointer) | Light (output refs) | N/A |
|
|
332
|
+
| Output freshness | Always live ✓ | Stale until redeployed* | Always live ✓ |
|
|
333
|
+
| Consumer code | `resolveStackRef(...)` | `new StackReference(...)` ✓ | Custom |
|
|
334
|
+
| CI/CD orchestration | Not needed | Recommended | Not needed |
|
|
335
|
+
|
|
336
|
+
\* *Mitigated with CI/CD automation — alias deployments are fast (seconds)*
|
|
337
|
+
|
|
338
|
+
## Benefits
|
|
339
|
+
|
|
340
|
+
✅ **Zero consumer dependencies** - Consumers use standard Pulumi `StackReference`
|
|
341
|
+
✅ **Producer-controlled** - Infrastructure projects own aliasing decisions
|
|
342
|
+
✅ **Type-safe** - Full TypeScript support
|
|
343
|
+
✅ **Flexible patterns** - Wildcard pattern matching for conditional aliasing
|
|
344
|
+
✅ **CI/CD friendly** - Fast alias deployments (no real resources)
|
|
345
|
+
✅ **Simple API** - Three functions cover all use cases
|
|
346
|
+
|
|
347
|
+
## Trade-offs
|
|
348
|
+
|
|
349
|
+
**Pros:**
|
|
350
|
+
- Zero consumer library dependency
|
|
351
|
+
- Standard Pulumi patterns
|
|
352
|
+
- Simple mental model
|
|
353
|
+
|
|
354
|
+
**Cons:**
|
|
355
|
+
- Alias stacks need redeployment when canonical outputs change (mitigated with CI/CD)
|
|
356
|
+
- Slightly more operational overhead than redirect pattern (but eliminates consumer dependencies)
|
|
357
|
+
|
|
358
|
+
## Use Cases
|
|
359
|
+
|
|
360
|
+
### Shared Development Infrastructure
|
|
361
|
+
|
|
362
|
+
Deploy infrastructure once, route dev/staging to it:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
const alias = createConditionalAlias({
|
|
366
|
+
targetProject: "infrastructure",
|
|
367
|
+
patterns: [
|
|
368
|
+
{ pattern: "*/prod", target: "prod" },
|
|
369
|
+
],
|
|
370
|
+
defaultTarget: "shared",
|
|
371
|
+
outputs: ["vpcId", "endpoint"],
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Ephemeral PR Environments
|
|
376
|
+
|
|
377
|
+
Route ephemeral stacks to shared infrastructure:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
const alias = createConditionalAlias({
|
|
381
|
+
targetProject: "infrastructure",
|
|
382
|
+
patterns: [
|
|
383
|
+
{ pattern: "*/*-ephemeral", target: "shared" },
|
|
384
|
+
{ pattern: "*/prod", target: "prod" },
|
|
385
|
+
],
|
|
386
|
+
defaultTarget: "shared",
|
|
387
|
+
outputs: ["vpcId"],
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Multi-Region Deployments
|
|
392
|
+
|
|
393
|
+
Route regional stacks to regional infrastructure:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
const alias = createConditionalAlias({
|
|
397
|
+
targetProject: "infrastructure",
|
|
398
|
+
patterns: [
|
|
399
|
+
{ pattern: "*/us-*", target: "us-east" },
|
|
400
|
+
{ pattern: "*/eu-*", target: "eu-west" },
|
|
401
|
+
],
|
|
402
|
+
outputs: ["vpcId"],
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Examples
|
|
407
|
+
|
|
408
|
+
See the [examples](./examples) directory for complete implementations:
|
|
409
|
+
- [Simple Alias](./examples/simple-alias) - Basic proxy stack
|
|
410
|
+
- [Conditional Alias](./examples/conditional-alias) - Pattern-based aliasing
|
|
411
|
+
|
|
412
|
+
## Migration Guide
|
|
413
|
+
|
|
414
|
+
### From v0.1.0 (Redirect Pattern) to v0.2.0 (Proxy Pattern)
|
|
415
|
+
|
|
416
|
+
This is a **breaking change** — complete API rewrite.
|
|
417
|
+
|
|
418
|
+
#### Producer Changes
|
|
419
|
+
|
|
420
|
+
**Before (v0.1.0):**
|
|
421
|
+
```typescript
|
|
422
|
+
// infrastructure/index.ts
|
|
423
|
+
const config = new pulumi.Config();
|
|
424
|
+
const aliasTarget = config.get("aliasTarget");
|
|
425
|
+
|
|
426
|
+
if (aliasTarget) {
|
|
427
|
+
export const _canonicalStack = aliasTarget;
|
|
428
|
+
} else {
|
|
429
|
+
export const vpcId = vpc.id;
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**After (v0.2.0):**
|
|
434
|
+
```typescript
|
|
435
|
+
// infrastructure/index.ts
|
|
436
|
+
import { createStackAlias } from "@egulatee/pulumi-stack-alias";
|
|
437
|
+
|
|
438
|
+
const config = new pulumi.Config();
|
|
439
|
+
const aliasTarget = config.get("aliasTarget");
|
|
440
|
+
|
|
441
|
+
if (aliasTarget) {
|
|
442
|
+
const alias = createStackAlias({
|
|
443
|
+
targetProject: "infrastructure",
|
|
444
|
+
targetStack: aliasTarget,
|
|
445
|
+
outputs: ["vpcId", "endpoint"],
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
export const vpcId = alias.vpcId;
|
|
449
|
+
export const endpoint = alias.endpoint;
|
|
450
|
+
} else {
|
|
451
|
+
export const vpcId = vpc.id;
|
|
452
|
+
export const endpoint = /* ... */;
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
#### Consumer Changes
|
|
457
|
+
|
|
458
|
+
**Before (v0.1.0):**
|
|
459
|
+
```typescript
|
|
460
|
+
import { resolveStackRef } from "@egulatee/pulumi-stack-alias";
|
|
461
|
+
|
|
462
|
+
const infraStack = resolveStackRef("infrastructure");
|
|
463
|
+
const vpcId = infraStack.apply(ref => ref.requireOutput("vpcId"));
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**After (v0.2.0):**
|
|
467
|
+
```typescript
|
|
468
|
+
// NO LIBRARY IMPORT NEEDED!
|
|
469
|
+
const infraStack = new pulumi.StackReference(
|
|
470
|
+
`${pulumi.getOrganization()}/infrastructure/${pulumi.getStack()}`
|
|
471
|
+
);
|
|
472
|
+
const vpcId = infraStack.requireOutput("vpcId");
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
#### Migration Steps
|
|
476
|
+
|
|
477
|
+
1. **Update package.json**: Bump to `@egulatee/pulumi-stack-alias@^0.2.0`
|
|
478
|
+
2. **Update producer code**: Replace `_canonicalStack` exports with `createStackAlias()` calls
|
|
479
|
+
3. **Update consumer code**: Replace `resolveStackRef()` with standard `new StackReference()`
|
|
480
|
+
4. **Remove consumer dependency**: Uninstall `@egulatee/pulumi-stack-alias` from consumer projects
|
|
481
|
+
5. **Redeploy all stacks**: Alias stacks need one-time redeployment to export new outputs
|
|
482
|
+
6. **Set up CI/CD**: Add alias sync jobs to keep outputs fresh
|
|
483
|
+
|
|
484
|
+
## Contributing
|
|
485
|
+
|
|
486
|
+
Contributions are welcome! Please see the [GitHub issues](https://github.com/egulatee/pulumi-stack-alias/issues) for planned features and enhancements.
|
|
487
|
+
|
|
488
|
+
## License
|
|
489
|
+
|
|
490
|
+
MIT © Eric Gulatee
|
|
491
|
+
|
|
492
|
+
## Links
|
|
493
|
+
|
|
494
|
+
- [GitHub Repository](https://github.com/egulatee/pulumi-stack-alias)
|
|
495
|
+
- [npm Package](https://www.npmjs.com/package/@egulatee/pulumi-stack-alias)
|
|
496
|
+
- [Issue Tracker](https://github.com/egulatee/pulumi-stack-alias/issues)
|