@gambi97/keel-cli 0.3.4 → 0.3.5
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/README.md +27 -18
- package/dist/generate.js +24 -8
- package/dist/ui.js +2 -1
- package/package.json +1 -1
- package/templates/README.md +33 -14
- package/templates/_partials/apply-header.yml +7 -1
- package/templates/_partials/apply-job.yml +3 -2
package/README.md
CHANGED
|
@@ -62,7 +62,8 @@ non-empty repository is reported immediately, and only that answer is asked
|
|
|
62
62
|
again). It picks up `SCW_*` / `INFISICAL_*` / `GITHUB_TOKEN` from your
|
|
63
63
|
environment as defaults, then shows a **complete summary of what it will
|
|
64
64
|
create and where**. Nothing is touched before you confirm. When it finishes, push to `main` (or merge the
|
|
65
|
-
first PR) and the pipeline provisions the infrastructure
|
|
65
|
+
first PR) and the pipeline provisions the non-production infrastructure; a
|
|
66
|
+
version tag (`vX.Y.Z`) promotes to production.
|
|
66
67
|
|
|
67
68
|
Non-interactive and dry-run:
|
|
68
69
|
|
|
@@ -150,14 +151,16 @@ flowchart TD
|
|
|
150
151
|
CLI -->|creates state bucket| SCW[(Scaleway<br/>Object Storage)]
|
|
151
152
|
CLI -->|creates repo, secrets,<br/>variables, branch rules| GH[GitHub repo]
|
|
152
153
|
CLI -->|creates project, envs,<br/>placeholder secrets| INF[Infisical]
|
|
153
|
-
GH -->|
|
|
154
|
-
|
|
154
|
+
GH -->|merge to main| ACT[GitHub Actions]
|
|
155
|
+
GH -->|tag vX.Y.Z| ACT
|
|
156
|
+
ACT -->|apply non-prod / prod| PROD[Scaleway:<br/>Container + SQL + Registry]
|
|
155
157
|
INF -.->|app secrets at runtime| PROD
|
|
156
158
|
```
|
|
157
159
|
|
|
158
160
|
The CLI does the **bootstrap**, in seconds, on your machine. The first
|
|
159
161
|
`terraform apply` runs **in CI** on the first push to `main`, so nobody needs
|
|
160
|
-
local tooling or production credentials.
|
|
162
|
+
local tooling or production credentials. Non-production environments deploy on
|
|
163
|
+
every merge; production is promoted by pushing a version tag.
|
|
161
164
|
|
|
162
165
|
## What gets created, and when
|
|
163
166
|
|
|
@@ -167,7 +170,7 @@ local tooling or production credentials.
|
|
|
167
170
|
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
168
171
|
| Your machine | The generated repo: Terraform, workflows, README, initial git commit |
|
|
169
172
|
| Scaleway | One Object Storage bucket for Terraform state (versioned, with native state locking, restricted by a bucket policy to the identity behind your API key) |
|
|
170
|
-
| GitHub | Repository (public or private) pushed to `main`; encrypted Actions secrets; Actions variables; one deployment environment per selected environment (`production`
|
|
173
|
+
| GitHub | Repository (public or private) pushed to `main`; encrypted Actions secrets; Actions variables; one deployment environment per selected environment (`production` promoted by a version tag); branch protection on `main` |
|
|
171
174
|
| Infisical | A project with one environment per selected environment, seeded with `BASIC_AUTH_USER` / `BASIC_AUTH_PASSWORD` (non-production, random password), `DATABASE_URL` / `APP_URL` placeholders per environment, and `S3_*` placeholders when Object Storage is enabled |
|
|
172
175
|
|
|
173
176
|
**Phase B: first deploy (Terraform in GitHub Actions, on push to `main`)**
|
|
@@ -190,7 +193,7 @@ my-app/
|
|
|
190
193
|
├── README.md # operating manual for the repo
|
|
191
194
|
├── .github/workflows/
|
|
192
195
|
│ ├── terraform-plan.yml # PR: fmt + validate + plan (every environment)
|
|
193
|
-
│ ├── terraform-apply.yml # main: apply
|
|
196
|
+
│ ├── terraform-apply.yml # merge to main: apply non-prod; tag vX.Y.Z: apply prod
|
|
194
197
|
│ └── terraform-drift.yml # weekly: read-only plan, opens an issue on drift
|
|
195
198
|
├── .keel/manifest.json # committed record: keel version, contract version, options
|
|
196
199
|
├── versions.tf · providers.tf · backend.tf
|
|
@@ -222,8 +225,8 @@ day-2 work (scaling, rotating secrets, custom domains, troubleshooting). The
|
|
|
222
225
|
short version of the first deploy:
|
|
223
226
|
|
|
224
227
|
1. **Push to `main`** (or merge a PR): the pipeline provisions registry and
|
|
225
|
-
databases (and an Object Storage bucket,
|
|
226
|
-
|
|
228
|
+
databases for the non-production environments (and an Object Storage bucket,
|
|
229
|
+
if enabled). Production waits for a version tag (step 5).
|
|
227
230
|
2. **Build and push your app image** to the registry endpoint from the apply
|
|
228
231
|
output (replace `<env>` with your target environment):
|
|
229
232
|
```sh
|
|
@@ -234,9 +237,12 @@ short version of the first deploy:
|
|
|
234
237
|
3. **Set `container_image`** in the tfvars and open a PR: the next apply
|
|
235
238
|
creates the containers.
|
|
236
239
|
4. **Replace the placeholder secrets** in Infisical with real values. The app
|
|
237
|
-
reads `DATABASE_URL`, `BASIC_AUTH_*` and (if enabled) `S3_*` from
|
|
238
|
-
environment; non-production environments also receive
|
|
239
|
-
and enforce it.
|
|
240
|
+
reads `DATABASE_URL`, `APP_URL`, `BASIC_AUTH_*` and (if enabled) `S3_*` from
|
|
241
|
+
its environment; non-production environments also receive
|
|
242
|
+
`BASIC_AUTH_ENABLED=true` and enforce it.
|
|
243
|
+
5. **Release to production**: push a version tag — `git tag v1.0.0 && git push
|
|
244
|
+
--tags` — and the production apply runs against that commit. A merge to
|
|
245
|
+
`main` never deploys production.
|
|
240
246
|
|
|
241
247
|
## Prerequisites
|
|
242
248
|
|
|
@@ -293,7 +299,9 @@ creating anything**.
|
|
|
293
299
|
- Actions secrets are encrypted client-side (libsodium sealed box) before
|
|
294
300
|
upload.
|
|
295
301
|
- `main` is protected: force pushes and deletion blocked, PRs need a green
|
|
296
|
-
plan
|
|
302
|
+
plan. Production never deploys from a merge — it is gated behind a version
|
|
303
|
+
tag, a free promotion control that works on any GitHub plan (required-reviewer
|
|
304
|
+
rules need a paid plan on private repos) and lives auditably in the repo.
|
|
297
305
|
- Terraform state lives in a private, versioned bucket with S3-native state
|
|
298
306
|
locking (`use_lockfile`), so concurrent applies cannot corrupt it.
|
|
299
307
|
- The state bucket is **restricted by a bucket policy** to the identity that
|
|
@@ -373,12 +381,13 @@ endpoints. Credentials live in Infisical, the container gets
|
|
|
373
381
|
**Which environments do I get, and can I change them?**
|
|
374
382
|
You choose at creation: production only, staging + production (default), or
|
|
375
383
|
dev + staging + production — interactively or with `--environments`
|
|
376
|
-
(e.g. `--environments prod` or `--environments dev,staging,prod`).
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
384
|
+
(e.g. `--environments prod` or `--environments dev,staging,prod`). Non-production
|
|
385
|
+
environments deploy on merge to `main` and enable Basic Auth by default;
|
|
386
|
+
production is promoted by pushing a version tag (`vX.Y.Z`), never by a merge. To
|
|
387
|
+
add a non-production environment later: add a `<env>.tfvars` (the plan and drift
|
|
388
|
+
workflows discover environments from the tfvars files at the repo root
|
|
389
|
+
automatically), an Infisical environment with the same slug, and a `main`-gated
|
|
390
|
+
job in `terraform-apply.yml`.
|
|
382
391
|
|
|
383
392
|
**Can I extend the generated repo with my own Terraform?**
|
|
384
393
|
Yes, and additions never edit generated files: drop a new `.tf` file at the
|
package/dist/generate.js
CHANGED
|
@@ -99,16 +99,32 @@ function isSpecial(rel) {
|
|
|
99
99
|
* in deploy order, each `needs:` the previous so applies run sequentially.
|
|
100
100
|
*/
|
|
101
101
|
function renderApplyWorkflow(source, answers, globalTokens) {
|
|
102
|
-
|
|
102
|
+
// Non-production environments deploy on merge to main (chained in order);
|
|
103
|
+
// production is promoted by a version tag. The triggers and the per-job
|
|
104
|
+
// guards are assembled from which environments exist, so a prod-only repo
|
|
105
|
+
// has no branch trigger and a prod-less selection has no tag trigger.
|
|
106
|
+
const nonProd = answers.environments.filter((env) => !env.production);
|
|
107
|
+
const prod = answers.environments.find((env) => env.production);
|
|
108
|
+
const triggers = [];
|
|
109
|
+
if (nonProd.length > 0)
|
|
110
|
+
triggers.push(' branches: [main]');
|
|
111
|
+
if (prod)
|
|
112
|
+
triggers.push(' tags: ["v*.*.*"]');
|
|
113
|
+
const header = renderContent(readFileSync(join(source, '_partials/apply-header.yml'), 'utf8'), { ...globalTokens, __APPLY_TRIGGERS__: triggers.join('\n') }, '_partials/apply-header.yml');
|
|
103
114
|
const jobTemplate = readFileSync(join(source, '_partials/apply-job.yml'), 'utf8');
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
const renderJob = (env, applyIf, needs) => renderContent(jobTemplate, {
|
|
116
|
+
__ENV_SLUG__: env.slug,
|
|
117
|
+
__GH_ENVIRONMENT__: env.githubEnvironment,
|
|
118
|
+
__APPLY_IF__: applyIf,
|
|
119
|
+
__NEEDS_LINE__: needs,
|
|
120
|
+
}, '_partials/apply-job.yml');
|
|
121
|
+
const jobs = nonProd.map((env, i) => {
|
|
122
|
+
const previous = nonProd[i - 1];
|
|
123
|
+
return renderJob(env, "github.ref == 'refs/heads/main'", previous ? ` needs: apply-${previous.slug}\n` : '');
|
|
111
124
|
});
|
|
125
|
+
if (prod) {
|
|
126
|
+
jobs.push(renderJob(prod, "startsWith(github.ref, 'refs/tags/')", ''));
|
|
127
|
+
}
|
|
112
128
|
return header + jobs.join('\n');
|
|
113
129
|
}
|
|
114
130
|
function renderContent(content, tokens, file) {
|
package/dist/ui.js
CHANGED
|
@@ -79,9 +79,10 @@ export function renderNextSteps(answers, repoUrl, containerUrlHint) {
|
|
|
79
79
|
` registry created by the first pipeline run (${containerUrlHint}).`,
|
|
80
80
|
` 2. Replace the placeholder secrets in Infisical (${answers.infisical.host})`,
|
|
81
81
|
` project "${answers.infisical.projectName}" with real values.`,
|
|
82
|
-
` 3. Push to main (or merge a PR) to
|
|
82
|
+
` 3. Push to main (or merge a PR) to deploy the non-production environments:`,
|
|
83
83
|
` ${repoUrl}/actions`,
|
|
84
84
|
` 4. Set container_image in ${answers.environments.map((e) => `${e.slug}.tfvars`).join(' / ')} once an image exists.`,
|
|
85
|
+
` 5. Release to production with a version tag: git tag v1.0.0 && git push --tags`,
|
|
85
86
|
'',
|
|
86
87
|
`Repository: ${repoUrl}`,
|
|
87
88
|
].join('\n');
|
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -40,12 +40,18 @@ Credentials live in GitHub Actions encrypted secrets (CI) and in Infisical
|
|
|
40
40
|
|
|
41
41
|
```
|
|
42
42
|
PR to main -> terraform fmt + validate + plan (every environment), shown as PR checks
|
|
43
|
-
merge/push main -> apply
|
|
43
|
+
merge/push main -> apply non-production environments in order (dev, staging)
|
|
44
|
+
tag vX.Y.Z -> apply production (the commit the tag points at)
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
- `main` is protected: PRs need a green plan, force pushes are blocked.
|
|
47
|
-
-
|
|
48
|
-
|
|
48
|
+
- Non-production environments apply on every merge to `main`, in order, each
|
|
49
|
+
`needs:` the previous one — a continuously-deployed staging.
|
|
50
|
+
- **Production is promoted by a version tag**, not by a merge: push a tag like
|
|
51
|
+
`v1.2.0` and the production apply runs against that commit. The tag is the
|
|
52
|
+
gate — it works on any GitHub plan (unlike required-reviewer rules, which
|
|
53
|
+
need a paid plan on private repos), lives in the repo, and rolling back is
|
|
54
|
+
re-tagging an earlier commit. Cut one with `git tag v1.2.0 && git push --tags`.
|
|
49
55
|
- After each apply, the pipeline syncs the secrets Terraform produced to
|
|
50
56
|
Infisical for that environment: a ready-to-use Postgres `DATABASE_URL`
|
|
51
57
|
(whose credential is a dedicated IAM application that can only read/write
|
|
@@ -60,9 +66,10 @@ merge/push main -> apply each environment in order; any gated environment (pro
|
|
|
60
66
|
## First deploy
|
|
61
67
|
|
|
62
68
|
1. **Merge or push to `main`.** The first pipeline run creates registry and
|
|
63
|
-
databases (and the Object Storage
|
|
64
|
-
Container is intentionally skipped until
|
|
65
|
-
is empty), so the first apply is green.
|
|
69
|
+
databases for the non-production environments (and the Object Storage
|
|
70
|
+
bucket, if enabled). The Serverless Container is intentionally skipped until
|
|
71
|
+
an image exists (`container_image` is empty), so the first apply is green.
|
|
72
|
+
Production waits for a version tag (step 5).
|
|
66
73
|
|
|
67
74
|
2. **Build and push the app image** (any Dockerfile, listening on port 8080
|
|
68
75
|
by default), replacing `<env>` with your target environment (e.g. staging):
|
|
@@ -90,7 +97,19 @@ merge/push main -> apply each environment in order; any gated environment (pro
|
|
|
90
97
|
|
|
91
98
|
Every secret in the Infisical environment is injected into the container
|
|
92
99
|
as a secret environment variable, so adding an app secret is: add it in
|
|
93
|
-
Infisical, re-run the apply (
|
|
100
|
+
Infisical, re-run the apply (a merge to `main` for non-prod, a new tag for
|
|
101
|
+
production).
|
|
102
|
+
|
|
103
|
+
5. **Release to production**: once staging looks good, cut a version tag on the
|
|
104
|
+
commit you want live and push it:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
git tag v1.0.0
|
|
108
|
+
git push --tags
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The production apply runs against that commit. Nothing reaches production
|
|
112
|
+
without a tag, so a merge to `main` can never deploy it by accident.
|
|
94
113
|
|
|
95
114
|
## Basic Auth on non-production environments
|
|
96
115
|
|
|
@@ -115,13 +134,13 @@ environment enables it is the `enable_basic_auth` flag in its `<env>.tfvars`.
|
|
|
115
134
|
secrets (repo Settings > Secrets and variables > Actions).
|
|
116
135
|
- **Add a custom domain**: add a `scaleway_container_domain` resource in
|
|
117
136
|
`modules/app_stack` pointing at the container, plus your DNS record.
|
|
118
|
-
- **Add
|
|
119
|
-
drift workflows discover environments from the tfvars files
|
|
120
|
-
create an Infisical environment with the same slug, and add a
|
|
121
|
-
`.github/workflows/terraform-apply.yml`
|
|
122
|
-
`
|
|
123
|
-
(<env>)` check becomes required on
|
|
124
|
-
branch-protection settings.
|
|
137
|
+
- **Add a non-production environment**: add a `<env>.tfvars` at the repo root
|
|
138
|
+
(the plan and drift workflows discover environments from the tfvars files
|
|
139
|
+
automatically), create an Infisical environment with the same slug, and add a
|
|
140
|
+
job in `.github/workflows/terraform-apply.yml` — mirror an existing non-prod
|
|
141
|
+
job (`if: github.ref == 'refs/heads/main'`) and set its `needs:` to chain
|
|
142
|
+
after the previous one. Note: the new `plan (<env>)` check becomes required on
|
|
143
|
+
`main` only after you add it in the branch-protection settings.
|
|
125
144
|
- **Pin provider versions**: after any local `terraform init`, commit the
|
|
126
145
|
generated `.terraform.lock.hcl` so CI resolves the exact same provider
|
|
127
146
|
builds on every run.
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
name: Terraform apply
|
|
2
2
|
|
|
3
|
+
# Non-production environments deploy on every merge to main; production is
|
|
4
|
+
# promoted by pushing a version tag (vX.Y.Z), so a merge never touches prod.
|
|
5
|
+
# Tag-based promotion is the free, git-native gate (no paid environment rule):
|
|
6
|
+
# the tag lives in the repo, is auditable, and rolling back is re-tagging.
|
|
3
7
|
on:
|
|
4
8
|
push:
|
|
5
|
-
|
|
9
|
+
__APPLY_TRIGGERS__
|
|
6
10
|
|
|
7
11
|
permissions:
|
|
8
12
|
contents: read
|
|
9
13
|
|
|
10
14
|
concurrency:
|
|
15
|
+
# Serialize all applies against the shared state bucket; a tag deploy and a
|
|
16
|
+
# main deploy never run at the same time.
|
|
11
17
|
group: terraform-apply
|
|
12
18
|
cancel-in-progress: false
|
|
13
19
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
apply-__ENV_SLUG__:
|
|
2
2
|
name: apply (__ENV_SLUG__)
|
|
3
3
|
runs-on: ubuntu-latest
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# Non-prod runs on merge to main; prod runs only on a version tag.
|
|
5
|
+
if: __APPLY_IF__
|
|
6
|
+
__NEEDS_LINE__ environment: __GH_ENVIRONMENT__
|
|
6
7
|
steps:
|
|
7
8
|
- uses: actions/checkout@v4
|
|
8
9
|
|