generic-gitlab-cicd 0.3.2__tar.gz → 0.3.4__tar.gz
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.
- generic_gitlab_cicd-0.3.4/PKG-INFO +194 -0
- generic_gitlab_cicd-0.3.4/README.md +183 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/__init__.py +1 -1
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/cli.py +8 -1
- generic_gitlab_cicd-0.3.4/generic_ci/setup.py +209 -0
- generic_gitlab_cicd-0.3.4/generic_gitlab_cicd.egg-info/PKG-INFO +194 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_gitlab_cicd.egg-info/SOURCES.txt +2 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/pyproject.toml +1 -1
- generic_gitlab_cicd-0.3.4/tests/test_setup.py +64 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/tests/test_sources.py +10 -0
- generic_gitlab_cicd-0.3.2/PKG-INFO +0 -170
- generic_gitlab_cicd-0.3.2/README.md +0 -159
- generic_gitlab_cicd-0.3.2/generic_gitlab_cicd.egg-info/PKG-INFO +0 -170
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/__main__.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/compiler.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/config.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/dependencies.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/models.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/runtime.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/sources.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/workflows/__init__.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/workflows/compiler.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/workflows/ecosystems.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/workflows/helm.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/workflows/models.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/workflows/publication.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_ci/workflows/runtime.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_gitlab_cicd.egg-info/dependency_links.txt +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_gitlab_cicd.egg-info/entry_points.txt +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_gitlab_cicd.egg-info/requires.txt +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/generic_gitlab_cicd.egg-info/top_level.txt +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/setup.cfg +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/tests/test_chart.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/tests/test_framework.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/tests/test_product.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/tests/test_publication.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/tests/test_review.py +0 -0
- {generic_gitlab_cicd-0.3.2 → generic_gitlab_cicd-0.3.4}/tests/test_workflows.py +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: generic-gitlab-cicd
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: Validated project configuration and reproducible GitLab delivery pipelines
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
8
|
+
Requires-Dist: PyYAML<7,>=6
|
|
9
|
+
Requires-Dist: tomlkit<1,>=0.13
|
|
10
|
+
Requires-Dist: packaging<27,>=25
|
|
11
|
+
|
|
12
|
+
# Generic GitLab CI/CD
|
|
13
|
+
|
|
14
|
+
**Define your projects and checks. Generate a reviewable GitLab pipeline.**
|
|
15
|
+
|
|
16
|
+
Generic GitLab CI/CD is a Python toolkit that turns a small delivery configuration into a committed `.gitlab-ci.yml`. It connects your existing test commands, application builds, package publication, container builds, and Helm deployments while keeping infrastructure settings separate from application configuration.
|
|
17
|
+
|
|
18
|
+
The package is named `generic-gitlab-cicd`; the command is `generic-ci`. GitLab and your runners execute the generated pipeline. The CLI validates and generates configuration locally.
|
|
19
|
+
|
|
20
|
+
[Quick start](#quick-start) · [Workflow guide](docs/workflows-revision-one.md) · [CLI reference](docs/cli-reference.md) · [Examples](examples/workflows) · [Documentation](docs/README.md)
|
|
21
|
+
|
|
22
|
+
## One repository. Tests, packages, and deployment.
|
|
23
|
+
|
|
24
|
+
Keep your shared Python package and API together. Generic CI tests them, publishes the package on release, and deploys the API to OpenShift.
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
version: 1
|
|
28
|
+
|
|
29
|
+
projects:
|
|
30
|
+
sdk:
|
|
31
|
+
path: packages/sdk
|
|
32
|
+
python: {}
|
|
33
|
+
checks:
|
|
34
|
+
tests:
|
|
35
|
+
script: [uv run --no-sync pytest]
|
|
36
|
+
package:
|
|
37
|
+
index: internal
|
|
38
|
+
release:
|
|
39
|
+
tag: v{version}
|
|
40
|
+
workflows:
|
|
41
|
+
merge-request:
|
|
42
|
+
checks: [tests]
|
|
43
|
+
release:
|
|
44
|
+
checks: [tests]
|
|
45
|
+
publish: true
|
|
46
|
+
|
|
47
|
+
api:
|
|
48
|
+
path: services/api
|
|
49
|
+
depends-on: [sdk]
|
|
50
|
+
python: {}
|
|
51
|
+
checks:
|
|
52
|
+
tests:
|
|
53
|
+
script: [uv run --no-sync pytest]
|
|
54
|
+
container:
|
|
55
|
+
dockerfile: Dockerfile
|
|
56
|
+
release:
|
|
57
|
+
tag: v{version}
|
|
58
|
+
needs: [sdk]
|
|
59
|
+
workflows:
|
|
60
|
+
merge-request:
|
|
61
|
+
checks: [tests]
|
|
62
|
+
release:
|
|
63
|
+
checks: [tests]
|
|
64
|
+
build: [container]
|
|
65
|
+
|
|
66
|
+
deployments:
|
|
67
|
+
api:
|
|
68
|
+
target: production
|
|
69
|
+
chart:
|
|
70
|
+
path: deploy/chart
|
|
71
|
+
values: [deploy/values.yaml]
|
|
72
|
+
images:
|
|
73
|
+
- from: api.build-image
|
|
74
|
+
set:
|
|
75
|
+
repository: apps.api.image.repository
|
|
76
|
+
tag: apps.api.image.tag
|
|
77
|
+
workflows:
|
|
78
|
+
release:
|
|
79
|
+
when: manual
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**What you get:**
|
|
83
|
+
|
|
84
|
+
- **On merge requests:** test affected projects. SDK changes also select the API for testing.
|
|
85
|
+
- **On a release tag:** run checks, publish the SDK, and build the API image. The API release depends on SDK publication; its tests and image build can run independently.
|
|
86
|
+
- **When you approve deployment:** deploy the built API image through Helm to OpenShift.
|
|
87
|
+
|
|
88
|
+
Both projects use a shared version: projects at `1.2.0` release under the protected tag `v1.2.0`. Dependency installation follows each project's declared dependencies and committed lockfile; `depends-on` selects work, not package replacements.
|
|
89
|
+
|
|
90
|
+
Your organization supplies prepared runtime images, registry settings, and the production target in `ci-platform.yml`. The application's `deploy/chart` and `deploy/values.yaml` describe the deployment. Start with the [shared chart](charts/generic-app) and [values example](examples/helm-values.yaml); configure the named `internal` publishing index and CI credentials through your platform.
|
|
91
|
+
|
|
92
|
+
## Quick start
|
|
93
|
+
|
|
94
|
+
Use Python **3.11 or newer**. From a checkout of this toolkit, install the CLI into an isolated environment:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
python -m venv .venv
|
|
98
|
+
. .venv/bin/activate
|
|
99
|
+
python -m pip install .
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then, from your application repository:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
generic-ci setup
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Setup guides you through organization templates or standalone configuration, previews files before writing, and includes local editor schemas. Start with its single-app configuration and add projects as needed, or choose an organization template for your monorepo. See the [setup guide](docs/setup-revision-one.md) for prepared images, unattended options, and editor integration.
|
|
109
|
+
|
|
110
|
+
After editing your configuration:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
generic-ci validate
|
|
114
|
+
generic-ci render -o .gitlab-ci.yml
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Review and commit `delivery.yml`, your platform configuration, and the generated `.gitlab-ci.yml` together. Validate the generated pipeline with your GitLab CI Lint, then push your branch.
|
|
118
|
+
|
|
119
|
+
**Prefer autocomplete?** Setup exports the schemas automatically. You can also refresh them from the installed CLI:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
generic-ci schema -o .generic-ci/delivery.schema.json
|
|
123
|
+
generic-ci schema --platform-schema -o .generic-ci/platform.schema.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Associate them with `delivery.yml` and `ci-platform.yml` in your editor. The [editor guide](docs/setup-revision-one.md#editor-completion-and-validation) includes PyCharm instructions.
|
|
127
|
+
|
|
128
|
+
## Add the delivery features you need
|
|
129
|
+
|
|
130
|
+
| Goal | Configuration / next step |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| Test Node, pnpm, or Bun projects | Declare `node.package-manager`, commit the corresponding lockfile, and use your existing commands; see the [workflow guide](docs/workflows-revision-one.md) |
|
|
133
|
+
| Build application artifacts | Set `build.script` and `build.outputs`; select `build: [application]` in the workflow |
|
|
134
|
+
| Retest downstream projects | Add project `depends-on: [sdk]`; this propagates change selection |
|
|
135
|
+
| Transfer generated files to another job | Add check/build `needs: [sdk.build]`; the producer must be enabled in the same event |
|
|
136
|
+
| Build and push containers | Configure `container` and select `build: [container]`; the default builder is Buildah |
|
|
137
|
+
| Publish Python or npm-compatible packages | Configure `package` and workflow `publish`; see [publication behavior](docs/workflows-revision-one.md#package-publication--revision-four) |
|
|
138
|
+
| Coordinate releases | Configure project versions, tag conventions, and optional `release.needs`; see the [release workflow](docs/workflows-revision-one.md#version-bumps-and-release-button) |
|
|
139
|
+
| Deploy to OpenShift | Configure a Helm deployment, target, values, and image bindings; start with the [deployment example](examples/workflows/delivery.yml) and [chart values](examples/helm-values.yaml) |
|
|
140
|
+
|
|
141
|
+
`depends-on` and `needs` have different jobs: the first selects affected projects; the second transfers explicit same-event outputs. Neither implicitly installs an unreleased package. Candidate dependency testing has a separate contract described in the [workflow guide](docs/workflows-revision-one.md).
|
|
142
|
+
|
|
143
|
+
## Share organization defaults
|
|
144
|
+
|
|
145
|
+
An organization source can provide platform defaults and starter templates. Initialize from a reviewed source revision:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
generic-ci init --repo ssh://git@gitlab.example.internal/platform/ci-config.git \
|
|
149
|
+
--ref v1.0.0 --template python-service
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The repository, ref, and template name above are examples; the source must contain the toolkit's source manifest. Initialization copies starter files and records an exact source commit. Commit the resulting descriptor and lockfile. Later source updates change inherited defaults while preserving consumer-owned template files.
|
|
153
|
+
|
|
154
|
+
For disconnected authoring, cache the locked source or import a Git bundle, then render with `--offline`. This flag controls configuration-source acquisition; it does not enforce network isolation inside application jobs. See [configuration sources](docs/configuration-sources-revision-two.md) for source layout, registration, updates, and offline setup.
|
|
155
|
+
|
|
156
|
+
## OpenShift deployment
|
|
157
|
+
|
|
158
|
+
The bundled [generic-app chart](charts/generic-app) supports multiple applications, Services, OpenShift Routes with TLS, probes, resources, ports, volumes/mounts, and pod labels/annotations. Images use **repository + tag**; Buildah also records the pushed digest as evidence. Security contexts are configurable, with cluster defaults applying when unset.
|
|
159
|
+
|
|
160
|
+
Helm deployments support complete updates and partial updates that preserve unchanged service images. Partial updates require a compatible existing baseline; chart or configuration changes require a complete deployment. Production requires a protected ref, and candidate dependency runs cannot update persistent deployments.
|
|
161
|
+
|
|
162
|
+
You supply registry credentials, CA trust, a kubeconfig, namespace resources, and application values. The toolkit does not provision the cluster, issue certificates, create PVCs automatically, or make multi-project publication transactional. Keep release image tags immutable: the shared chart uses `IfNotPresent`.
|
|
163
|
+
|
|
164
|
+
## Existing component users
|
|
165
|
+
|
|
166
|
+
The repository also contains [low-level GitLab CI/CD components](templates) and the older compiler, available through `--format legacy`. They are separate interfaces from the workflow configuration above. New users should start with the CLI walkthrough.
|
|
167
|
+
|
|
168
|
+
Mirror components into your GitLab instance and pin includes to an immutable revision. Consult the [component air-gap guide](docs/airgap.md) for setup. Older BuildKit/digest-only Helm adapters target chart 1.x; the current chart uses OpenShift Routes and repository/tag values. Do not mix those contracts without migrating the configuration.
|
|
169
|
+
|
|
170
|
+
## Development and validation
|
|
171
|
+
|
|
172
|
+
From a toolkit checkout with Python, Git, Node/npm, rsync, and Helm available:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
python -m pip install . build twine uv setuptools
|
|
176
|
+
npm ci --ignore-scripts --no-audit --no-fund
|
|
177
|
+
python scripts/sync_embedded.py --check
|
|
178
|
+
python scripts/sync_authoring_skill.py --check
|
|
179
|
+
python -m unittest discover -s tests -v
|
|
180
|
+
npm run test:ci-local
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The local pipeline fixtures use pinned `gitlab-ci-local`. Real GitLab/Runner E2E runs are deliberate, through workflow dispatch or an opt-in PR label. Use focused tests while iterating; see [testing commands and evidence](docs/testing-revision-one.md) for prerequisites, integration tests, release checks, and scenario selection.
|
|
184
|
+
|
|
185
|
+
Version 0.3.2 passed [fast CI](https://github.com/Heknon/generic-gitlab-cicd/actions/runs/33975049919) and [six real GitLab E2E scenarios](https://github.com/Heknon/generic-gitlab-cicd/actions/runs/33975818144). That evidence covers the tested pipeline fixtures, not deployment to your registry or OpenShift cluster. Qualify Buildah execution, registry access, Route admission, and rollout/rollback on your infrastructure before production use. Workflow runtime execution currently targets Linux; Windows workflow execution is not supported.
|
|
186
|
+
|
|
187
|
+
## Documentation
|
|
188
|
+
|
|
189
|
+
- [CLI commands, file ownership, and path rules](docs/cli-reference.md)
|
|
190
|
+
- [Checks, builds, releases, and deployment workflows](docs/workflows-revision-one.md)
|
|
191
|
+
- [Organization configuration sources](docs/configuration-sources-revision-two.md)
|
|
192
|
+
- [AI-assisted authoring and portable skill](docs/ai-authoring-revision-three.md)
|
|
193
|
+
- [Publishing the toolkit to PyPI](docs/pypi-release.md)
|
|
194
|
+
- [Full documentation map](docs/README.md)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Generic GitLab CI/CD
|
|
2
|
+
|
|
3
|
+
**Define your projects and checks. Generate a reviewable GitLab pipeline.**
|
|
4
|
+
|
|
5
|
+
Generic GitLab CI/CD is a Python toolkit that turns a small delivery configuration into a committed `.gitlab-ci.yml`. It connects your existing test commands, application builds, package publication, container builds, and Helm deployments while keeping infrastructure settings separate from application configuration.
|
|
6
|
+
|
|
7
|
+
The package is named `generic-gitlab-cicd`; the command is `generic-ci`. GitLab and your runners execute the generated pipeline. The CLI validates and generates configuration locally.
|
|
8
|
+
|
|
9
|
+
[Quick start](#quick-start) · [Workflow guide](docs/workflows-revision-one.md) · [CLI reference](docs/cli-reference.md) · [Examples](examples/workflows) · [Documentation](docs/README.md)
|
|
10
|
+
|
|
11
|
+
## One repository. Tests, packages, and deployment.
|
|
12
|
+
|
|
13
|
+
Keep your shared Python package and API together. Generic CI tests them, publishes the package on release, and deploys the API to OpenShift.
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
version: 1
|
|
17
|
+
|
|
18
|
+
projects:
|
|
19
|
+
sdk:
|
|
20
|
+
path: packages/sdk
|
|
21
|
+
python: {}
|
|
22
|
+
checks:
|
|
23
|
+
tests:
|
|
24
|
+
script: [uv run --no-sync pytest]
|
|
25
|
+
package:
|
|
26
|
+
index: internal
|
|
27
|
+
release:
|
|
28
|
+
tag: v{version}
|
|
29
|
+
workflows:
|
|
30
|
+
merge-request:
|
|
31
|
+
checks: [tests]
|
|
32
|
+
release:
|
|
33
|
+
checks: [tests]
|
|
34
|
+
publish: true
|
|
35
|
+
|
|
36
|
+
api:
|
|
37
|
+
path: services/api
|
|
38
|
+
depends-on: [sdk]
|
|
39
|
+
python: {}
|
|
40
|
+
checks:
|
|
41
|
+
tests:
|
|
42
|
+
script: [uv run --no-sync pytest]
|
|
43
|
+
container:
|
|
44
|
+
dockerfile: Dockerfile
|
|
45
|
+
release:
|
|
46
|
+
tag: v{version}
|
|
47
|
+
needs: [sdk]
|
|
48
|
+
workflows:
|
|
49
|
+
merge-request:
|
|
50
|
+
checks: [tests]
|
|
51
|
+
release:
|
|
52
|
+
checks: [tests]
|
|
53
|
+
build: [container]
|
|
54
|
+
|
|
55
|
+
deployments:
|
|
56
|
+
api:
|
|
57
|
+
target: production
|
|
58
|
+
chart:
|
|
59
|
+
path: deploy/chart
|
|
60
|
+
values: [deploy/values.yaml]
|
|
61
|
+
images:
|
|
62
|
+
- from: api.build-image
|
|
63
|
+
set:
|
|
64
|
+
repository: apps.api.image.repository
|
|
65
|
+
tag: apps.api.image.tag
|
|
66
|
+
workflows:
|
|
67
|
+
release:
|
|
68
|
+
when: manual
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**What you get:**
|
|
72
|
+
|
|
73
|
+
- **On merge requests:** test affected projects. SDK changes also select the API for testing.
|
|
74
|
+
- **On a release tag:** run checks, publish the SDK, and build the API image. The API release depends on SDK publication; its tests and image build can run independently.
|
|
75
|
+
- **When you approve deployment:** deploy the built API image through Helm to OpenShift.
|
|
76
|
+
|
|
77
|
+
Both projects use a shared version: projects at `1.2.0` release under the protected tag `v1.2.0`. Dependency installation follows each project's declared dependencies and committed lockfile; `depends-on` selects work, not package replacements.
|
|
78
|
+
|
|
79
|
+
Your organization supplies prepared runtime images, registry settings, and the production target in `ci-platform.yml`. The application's `deploy/chart` and `deploy/values.yaml` describe the deployment. Start with the [shared chart](charts/generic-app) and [values example](examples/helm-values.yaml); configure the named `internal` publishing index and CI credentials through your platform.
|
|
80
|
+
|
|
81
|
+
## Quick start
|
|
82
|
+
|
|
83
|
+
Use Python **3.11 or newer**. From a checkout of this toolkit, install the CLI into an isolated environment:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
python -m venv .venv
|
|
87
|
+
. .venv/bin/activate
|
|
88
|
+
python -m pip install .
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then, from your application repository:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
generic-ci setup
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Setup guides you through organization templates or standalone configuration, previews files before writing, and includes local editor schemas. Start with its single-app configuration and add projects as needed, or choose an organization template for your monorepo. See the [setup guide](docs/setup-revision-one.md) for prepared images, unattended options, and editor integration.
|
|
98
|
+
|
|
99
|
+
After editing your configuration:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
generic-ci validate
|
|
103
|
+
generic-ci render -o .gitlab-ci.yml
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Review and commit `delivery.yml`, your platform configuration, and the generated `.gitlab-ci.yml` together. Validate the generated pipeline with your GitLab CI Lint, then push your branch.
|
|
107
|
+
|
|
108
|
+
**Prefer autocomplete?** Setup exports the schemas automatically. You can also refresh them from the installed CLI:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
generic-ci schema -o .generic-ci/delivery.schema.json
|
|
112
|
+
generic-ci schema --platform-schema -o .generic-ci/platform.schema.json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Associate them with `delivery.yml` and `ci-platform.yml` in your editor. The [editor guide](docs/setup-revision-one.md#editor-completion-and-validation) includes PyCharm instructions.
|
|
116
|
+
|
|
117
|
+
## Add the delivery features you need
|
|
118
|
+
|
|
119
|
+
| Goal | Configuration / next step |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| Test Node, pnpm, or Bun projects | Declare `node.package-manager`, commit the corresponding lockfile, and use your existing commands; see the [workflow guide](docs/workflows-revision-one.md) |
|
|
122
|
+
| Build application artifacts | Set `build.script` and `build.outputs`; select `build: [application]` in the workflow |
|
|
123
|
+
| Retest downstream projects | Add project `depends-on: [sdk]`; this propagates change selection |
|
|
124
|
+
| Transfer generated files to another job | Add check/build `needs: [sdk.build]`; the producer must be enabled in the same event |
|
|
125
|
+
| Build and push containers | Configure `container` and select `build: [container]`; the default builder is Buildah |
|
|
126
|
+
| Publish Python or npm-compatible packages | Configure `package` and workflow `publish`; see [publication behavior](docs/workflows-revision-one.md#package-publication--revision-four) |
|
|
127
|
+
| Coordinate releases | Configure project versions, tag conventions, and optional `release.needs`; see the [release workflow](docs/workflows-revision-one.md#version-bumps-and-release-button) |
|
|
128
|
+
| Deploy to OpenShift | Configure a Helm deployment, target, values, and image bindings; start with the [deployment example](examples/workflows/delivery.yml) and [chart values](examples/helm-values.yaml) |
|
|
129
|
+
|
|
130
|
+
`depends-on` and `needs` have different jobs: the first selects affected projects; the second transfers explicit same-event outputs. Neither implicitly installs an unreleased package. Candidate dependency testing has a separate contract described in the [workflow guide](docs/workflows-revision-one.md).
|
|
131
|
+
|
|
132
|
+
## Share organization defaults
|
|
133
|
+
|
|
134
|
+
An organization source can provide platform defaults and starter templates. Initialize from a reviewed source revision:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
generic-ci init --repo ssh://git@gitlab.example.internal/platform/ci-config.git \
|
|
138
|
+
--ref v1.0.0 --template python-service
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The repository, ref, and template name above are examples; the source must contain the toolkit's source manifest. Initialization copies starter files and records an exact source commit. Commit the resulting descriptor and lockfile. Later source updates change inherited defaults while preserving consumer-owned template files.
|
|
142
|
+
|
|
143
|
+
For disconnected authoring, cache the locked source or import a Git bundle, then render with `--offline`. This flag controls configuration-source acquisition; it does not enforce network isolation inside application jobs. See [configuration sources](docs/configuration-sources-revision-two.md) for source layout, registration, updates, and offline setup.
|
|
144
|
+
|
|
145
|
+
## OpenShift deployment
|
|
146
|
+
|
|
147
|
+
The bundled [generic-app chart](charts/generic-app) supports multiple applications, Services, OpenShift Routes with TLS, probes, resources, ports, volumes/mounts, and pod labels/annotations. Images use **repository + tag**; Buildah also records the pushed digest as evidence. Security contexts are configurable, with cluster defaults applying when unset.
|
|
148
|
+
|
|
149
|
+
Helm deployments support complete updates and partial updates that preserve unchanged service images. Partial updates require a compatible existing baseline; chart or configuration changes require a complete deployment. Production requires a protected ref, and candidate dependency runs cannot update persistent deployments.
|
|
150
|
+
|
|
151
|
+
You supply registry credentials, CA trust, a kubeconfig, namespace resources, and application values. The toolkit does not provision the cluster, issue certificates, create PVCs automatically, or make multi-project publication transactional. Keep release image tags immutable: the shared chart uses `IfNotPresent`.
|
|
152
|
+
|
|
153
|
+
## Existing component users
|
|
154
|
+
|
|
155
|
+
The repository also contains [low-level GitLab CI/CD components](templates) and the older compiler, available through `--format legacy`. They are separate interfaces from the workflow configuration above. New users should start with the CLI walkthrough.
|
|
156
|
+
|
|
157
|
+
Mirror components into your GitLab instance and pin includes to an immutable revision. Consult the [component air-gap guide](docs/airgap.md) for setup. Older BuildKit/digest-only Helm adapters target chart 1.x; the current chart uses OpenShift Routes and repository/tag values. Do not mix those contracts without migrating the configuration.
|
|
158
|
+
|
|
159
|
+
## Development and validation
|
|
160
|
+
|
|
161
|
+
From a toolkit checkout with Python, Git, Node/npm, rsync, and Helm available:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
python -m pip install . build twine uv setuptools
|
|
165
|
+
npm ci --ignore-scripts --no-audit --no-fund
|
|
166
|
+
python scripts/sync_embedded.py --check
|
|
167
|
+
python scripts/sync_authoring_skill.py --check
|
|
168
|
+
python -m unittest discover -s tests -v
|
|
169
|
+
npm run test:ci-local
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The local pipeline fixtures use pinned `gitlab-ci-local`. Real GitLab/Runner E2E runs are deliberate, through workflow dispatch or an opt-in PR label. Use focused tests while iterating; see [testing commands and evidence](docs/testing-revision-one.md) for prerequisites, integration tests, release checks, and scenario selection.
|
|
173
|
+
|
|
174
|
+
Version 0.3.2 passed [fast CI](https://github.com/Heknon/generic-gitlab-cicd/actions/runs/33975049919) and [six real GitLab E2E scenarios](https://github.com/Heknon/generic-gitlab-cicd/actions/runs/33975818144). That evidence covers the tested pipeline fixtures, not deployment to your registry or OpenShift cluster. Qualify Buildah execution, registry access, Route admission, and rollout/rollback on your infrastructure before production use. Workflow runtime execution currently targets Linux; Windows workflow execution is not supported.
|
|
175
|
+
|
|
176
|
+
## Documentation
|
|
177
|
+
|
|
178
|
+
- [CLI commands, file ownership, and path rules](docs/cli-reference.md)
|
|
179
|
+
- [Checks, builds, releases, and deployment workflows](docs/workflows-revision-one.md)
|
|
180
|
+
- [Organization configuration sources](docs/configuration-sources-revision-two.md)
|
|
181
|
+
- [AI-assisted authoring and portable skill](docs/ai-authoring-revision-three.md)
|
|
182
|
+
- [Publishing the toolkit to PyPI](docs/pypi-release.md)
|
|
183
|
+
- [Full documentation map](docs/README.md)
|
|
@@ -13,6 +13,13 @@ from .models import Pipeline, Platform
|
|
|
13
13
|
|
|
14
14
|
def main(argv=None):
|
|
15
15
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
16
|
+
if argv and argv[0] == 'setup':
|
|
17
|
+
from .setup import setup_main
|
|
18
|
+
try:
|
|
19
|
+
return setup_main(argv[1:])
|
|
20
|
+
except (ValueError, OSError, EOFError, ValidationError) as error:
|
|
21
|
+
print(f'generic-ci setup: {error}', file=sys.stderr)
|
|
22
|
+
return 1
|
|
16
23
|
if argv and argv[0] == 'source':
|
|
17
24
|
from .sources import source_main
|
|
18
25
|
try:
|
|
@@ -21,7 +28,7 @@ def main(argv=None):
|
|
|
21
28
|
print(f'generic-ci: {error}', file=sys.stderr)
|
|
22
29
|
return 1
|
|
23
30
|
parser = argparse.ArgumentParser(prog="generic-ci")
|
|
24
|
-
parser.add_argument("command", choices=["schema", "validate", "explain", "render", "init"])
|
|
31
|
+
parser.add_argument("command", choices=["schema", "validate", "explain", "render", "init", "setup"])
|
|
25
32
|
parser.add_argument("--config")
|
|
26
33
|
parser.add_argument("--format", choices=["workflows", "legacy"], default="workflows")
|
|
27
34
|
parser.add_argument("--ecosystem", choices=["python", "npm", "pnpm", "bun"], default="python")
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Guided configuration authoring; never executes application commands."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from .models import relative
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def setup_main(argv):
|
|
15
|
+
parser = argparse.ArgumentParser(prog='generic-ci setup', description=__doc__)
|
|
16
|
+
parser.add_argument('--root', default='.')
|
|
17
|
+
parser.add_argument('--mode', choices=['organization', 'standalone'])
|
|
18
|
+
parser.add_argument('--yes', action='store_true', help='Use supplied/default answers and write without prompting')
|
|
19
|
+
parser.add_argument('--dry-run', action='store_true', help='Validate and preview without writing project files')
|
|
20
|
+
parser.add_argument('--offline', action='store_true')
|
|
21
|
+
for flag in ['repo', 'ref', 'source', 'template', 'app', 'path', 'ecosystem', 'test-command',
|
|
22
|
+
'runtime-image', 'builder-image', 'registry', 'preview-registry', 'runner-tag',
|
|
23
|
+
'deploy', 'helm-image', 'chart-oci', 'chart-version', 'namespace', 'hostname', 'port', 'tls']:
|
|
24
|
+
parser.add_argument('--' + flag)
|
|
25
|
+
args = parser.parse_args(argv)
|
|
26
|
+
root = Path(args.root).absolute()
|
|
27
|
+
if not root.is_dir():
|
|
28
|
+
raise ValueError('--root must be an existing application directory')
|
|
29
|
+
interactive = not args.yes and sys.stdin.isatty()
|
|
30
|
+
if not interactive and not args.yes:
|
|
31
|
+
raise ValueError('non-interactive setup requires --yes (use --dry-run to preview)')
|
|
32
|
+
|
|
33
|
+
def ask(key, question, default=None, choices=None):
|
|
34
|
+
value = getattr(args, key.replace('-', '_'))
|
|
35
|
+
if value is None:
|
|
36
|
+
if interactive:
|
|
37
|
+
value = input(question + (f' [{default}]' if default is not None else '') + ': ').strip() or default
|
|
38
|
+
else:
|
|
39
|
+
value = default
|
|
40
|
+
if value is None or value == '':
|
|
41
|
+
raise ValueError(f'--{key} is required')
|
|
42
|
+
if choices and value not in choices:
|
|
43
|
+
raise ValueError(f'--{key} must be one of: {", ".join(choices)}')
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
mode = ask('mode', 'Setup path: organization or standalone', 'organization' if args.repo or args.source else 'standalone', ['organization', 'standalone'])
|
|
47
|
+
with tempfile.TemporaryDirectory(prefix='generic-ci-setup-') as temporary:
|
|
48
|
+
stage = Path(temporary)
|
|
49
|
+
if mode == 'organization':
|
|
50
|
+
from .sources import initialize
|
|
51
|
+
repo = None if args.source else ask('repo', 'Organization configuration Git repository (GitHub or GitLab URL)')
|
|
52
|
+
ref = ask('ref', 'Source revision') if repo else None
|
|
53
|
+
if not args.template:
|
|
54
|
+
initialize(stage, None, args.source, repo, ref, args.offline)
|
|
55
|
+
template = ask('template', 'Template name')
|
|
56
|
+
initialize(stage, template, args.source, repo, ref, args.offline)
|
|
57
|
+
descriptor = yaml.safe_load((stage / 'generic-ci.yml').read_text())
|
|
58
|
+
delivery_path = stage / descriptor['delivery']
|
|
59
|
+
config = yaml.safe_load(delivery_path.read_text())
|
|
60
|
+
projects = config.get('projects', {})
|
|
61
|
+
if projects:
|
|
62
|
+
selected = ask('app', 'Application in template to configure', next(iter(projects)), list(projects))
|
|
63
|
+
project = projects[selected]
|
|
64
|
+
project['path'] = relative(ask('path', 'Application directory', project.get('path', '.')))
|
|
65
|
+
checks = project.get('checks', {})
|
|
66
|
+
if len(checks) == 1:
|
|
67
|
+
check = next(iter(checks.values()))
|
|
68
|
+
existing = check.get('script', [])
|
|
69
|
+
if len(existing) == 1:
|
|
70
|
+
check['script'] = [ask('test-command', 'Confirm template check command', existing[0])]
|
|
71
|
+
elif args.test_command:
|
|
72
|
+
raise ValueError('template has multiple/no checks; edit its delivery configuration to select commands')
|
|
73
|
+
delivery_path.write_text(yaml.safe_dump(config, sort_keys=False))
|
|
74
|
+
if any(getattr(args, key) is not None for key in ['ecosystem', 'deploy', 'runtime_image', 'builder_image', 'registry', 'preview_registry', 'runner_tag', 'helm_image', 'chart_oci', 'chart_version', 'namespace', 'hostname', 'port', 'tls']):
|
|
75
|
+
raise ValueError('organization mode inherits infrastructure/deployment settings from its template; use standalone mode for infrastructure flags')
|
|
76
|
+
else:
|
|
77
|
+
if args.repo or args.ref or args.source or args.template:
|
|
78
|
+
raise ValueError('source options require organization mode')
|
|
79
|
+
name = ask('app', 'Application name', 'app')
|
|
80
|
+
path = relative(ask('path', 'Application directory relative to repository', '.'))
|
|
81
|
+
application = root / path
|
|
82
|
+
if not application.is_dir() or not application.resolve().is_relative_to(root.resolve()):
|
|
83
|
+
raise ValueError('application directory must exist inside the repository')
|
|
84
|
+
detected = 'python' if (application / 'pyproject.toml').exists() else 'npm' if (application / 'package.json').exists() else 'generic'
|
|
85
|
+
for lock, manager in [('pnpm-lock.yaml', 'pnpm'), ('bun.lock', 'bun')]:
|
|
86
|
+
if (application / lock).exists():
|
|
87
|
+
detected = manager
|
|
88
|
+
ecosystem = ask('ecosystem', 'Detected ecosystem (confirm or change)', detected, ['python', 'npm', 'pnpm', 'bun', 'generic'])
|
|
89
|
+
command = None
|
|
90
|
+
if ecosystem in ['npm', 'pnpm', 'bun'] and (application / 'package.json').exists():
|
|
91
|
+
if 'test' in json.loads((application / 'package.json').read_text()).get('scripts', {}):
|
|
92
|
+
command = ecosystem + ' run test'
|
|
93
|
+
test = ask('test-command', 'Test command (existing command; never executed by setup)', command)
|
|
94
|
+
runtime = ask('runtime-image', 'Prepared runtime image containing the matching toolkit')
|
|
95
|
+
builder = ask('builder-image', 'Prepared Buildah image containing the matching toolkit')
|
|
96
|
+
registry = ask('registry', 'Release container registry/repository prefix')
|
|
97
|
+
previews = ask('preview-registry', 'Preview container registry/repository prefix')
|
|
98
|
+
tag = ask('runner-tag', 'Runner tag')
|
|
99
|
+
project = {'path': path, 'checks': {'test': {'script': [test]}},
|
|
100
|
+
'workflows': {'push': {'checks': ['test']}, 'merge-request': {'checks': ['test']}}}
|
|
101
|
+
role = 'python' if ecosystem in ['python', 'generic'] else 'bun' if ecosystem == 'bun' else 'node'
|
|
102
|
+
if ecosystem == 'python':
|
|
103
|
+
project['python'] = {}
|
|
104
|
+
elif ecosystem != 'generic':
|
|
105
|
+
project['node'] = {'package-manager': ecosystem}
|
|
106
|
+
platform = {'version': 1, 'defaults': {'tags': [tag]}, 'images': {role: runtime},
|
|
107
|
+
'container-builder': {'image': builder},
|
|
108
|
+
'registries': {'containers': registry, 'previews': previews},
|
|
109
|
+
'allowed-hosts': sorted({x.split('/')[0] for x in [runtime, builder, registry, previews]}),
|
|
110
|
+
'variables': {'UV_PYTHON_DOWNLOADS': 'never'}}
|
|
111
|
+
delivery = {'version': 1, 'projects': {name: project}}
|
|
112
|
+
deploy = ask('deploy', 'Create an OpenShift MR preview deployment? yes/no', 'no', ['yes', 'no'])
|
|
113
|
+
if deploy == 'yes':
|
|
114
|
+
if not (application / 'Dockerfile').is_file():
|
|
115
|
+
raise ValueError('deployment setup requires an existing Dockerfile in the application directory')
|
|
116
|
+
helm = ask('helm-image', 'Prepared Helm image containing Git, Python and the toolkit')
|
|
117
|
+
chart = ask('chart-oci', 'Published generic-app 2.x compatible OCI chart URL')
|
|
118
|
+
version = ask('chart-version', 'Pinned chart version')
|
|
119
|
+
namespace = ask('namespace', 'Existing preview namespace')
|
|
120
|
+
hostname = ask('hostname', 'Route hostname (auto lets OpenShift assign a unique hostname)' )
|
|
121
|
+
port = int(ask('port', 'Application listening port', '8080'))
|
|
122
|
+
if not 1 <= port <= 65535:
|
|
123
|
+
raise ValueError('--port must be between 1 and 65535')
|
|
124
|
+
tls = ask('tls', 'Route TLS mode', 'edge', ['edge', 'reencrypt', 'passthrough', 'none'])
|
|
125
|
+
project['workflows']['merge-request']['build'] = ['container']
|
|
126
|
+
project['container'] = {'dockerfile': 'Dockerfile', 'context': '.'}
|
|
127
|
+
platform['images']['helm'] = helm
|
|
128
|
+
platform['allowed-hosts'] = sorted(set(platform['allowed-hosts'] + [helm.split('/')[0], urlsplit(chart).hostname or '']))
|
|
129
|
+
platform['targets'] = {'preview': {'namespace': namespace, 'production': False, 'kubeconfig-variable': 'PREVIEW_KUBECONFIG'}}
|
|
130
|
+
delivery['deployments'] = {name: {'target': 'preview', 'chart': {'oci': chart, 'version': version},
|
|
131
|
+
'values': ['deploy/values.yaml'], 'images': [{'from': name + '.build-image',
|
|
132
|
+
'set': {'repository': 'apps.' + name + '.image.repository', 'tag': 'apps.' + name + '.image.tag'}}],
|
|
133
|
+
'workflows': {'merge-request': {'when': 'manual'}}}}
|
|
134
|
+
# Helm values are literal: CI variables in a hostname are not expanded.
|
|
135
|
+
if '$' in hostname:
|
|
136
|
+
raise ValueError('Route hostname is literal; use auto for an OpenShift-generated hostname')
|
|
137
|
+
route = {'enabled': True}
|
|
138
|
+
if hostname != 'auto':
|
|
139
|
+
route['host'] = hostname
|
|
140
|
+
if tls != 'none':
|
|
141
|
+
route['tls'] = {'termination': tls, 'insecureEdgeTerminationPolicy': 'Redirect'}
|
|
142
|
+
values = {'apps': {name: {'enabled': True, 'replicas': 1, 'image': {'repository': previews + '/' + name, 'tag': 'setup-placeholder'},
|
|
143
|
+
'config': {}, 'secretRefs': [], 'service': {'enabled': True, 'port': 80, 'targetPort': port}, 'route': route}}}
|
|
144
|
+
(stage / 'deploy').mkdir()
|
|
145
|
+
(stage / 'deploy/values.yaml').write_text(yaml.safe_dump(values, sort_keys=False))
|
|
146
|
+
for filename, value in [('delivery.yml', delivery), ('ci-platform.yml', platform)]:
|
|
147
|
+
(stage / filename).write_text(yaml.safe_dump(value, sort_keys=False))
|
|
148
|
+
from .cli import main
|
|
149
|
+
schema_dir = stage / '.generic-ci'
|
|
150
|
+
schema_dir.mkdir(exist_ok=True)
|
|
151
|
+
for name, extra in [('delivery.schema.json', []), ('platform.schema.json', ['--platform-schema'])]:
|
|
152
|
+
target = schema_dir / name
|
|
153
|
+
if target.exists():
|
|
154
|
+
raise ValueError('template supplies a reserved setup schema path: ' + name)
|
|
155
|
+
if main(['schema', *extra, '-o', str(target)]):
|
|
156
|
+
raise ValueError('schema generation failed')
|
|
157
|
+
import os
|
|
158
|
+
descriptor = yaml.safe_load((stage / 'generic-ci.yml').read_text()) if (stage / 'generic-ci.yml').exists() else {}
|
|
159
|
+
for name, schema in [(descriptor.get('delivery', 'delivery.yml'), 'delivery.schema.json'),
|
|
160
|
+
(descriptor.get('platform') or 'ci-platform.yml', 'platform.schema.json')]:
|
|
161
|
+
target = stage / name
|
|
162
|
+
if target.is_file():
|
|
163
|
+
reference = Path(os.path.relpath(schema_dir / schema, target.parent)).as_posix()
|
|
164
|
+
target.write_text('# yaml-language-server: $schema=' + reference + '\n' + target.read_text())
|
|
165
|
+
if main(['validate', '--root', str(stage), *(['--offline'] if args.offline else [])]):
|
|
166
|
+
raise ValueError('setup configuration did not validate; no project files written')
|
|
167
|
+
if main(['render', '--root', str(stage), '-o', str(stage / '.gitlab-ci.yml'), *(['--offline'] if args.offline else [])]):
|
|
168
|
+
raise ValueError('setup rendering failed; no project files written')
|
|
169
|
+
notes = stage / 'CI-SETUP.md'
|
|
170
|
+
if not notes.exists():
|
|
171
|
+
notes.write_text('# CI setup — revision one\n\nReview delivery.yml and the generated .gitlab-ci.yml before committing.\n\n'
|
|
172
|
+
'- Provide matching toolkit runtime images, runner tags, registry authentication and CA trust.\n'
|
|
173
|
+
'- Local schemas: .generic-ci/delivery.schema.json and .generic-ci/platform.schema.json. In PyCharm Settings > Languages & Frameworks > Schemas and DTDs > JSON Schema Mappings, associate each schema with its YAML file. YAML-language-server editors can use the generated schema comment.\n'
|
|
174
|
+
'- Commit package-manager lockfiles and declare the dependencies needed by your checks.\n'
|
|
175
|
+
'- For previews, provide PREVIEW_KUBECONFIG as a GitLab file variable, an existing namespace, image pull secrets and a published compatible chart.\n'
|
|
176
|
+
'- Review Route host uniqueness, TLS certificates/backend trust, probes and application secrets in deploy/values.yaml when present.\n'
|
|
177
|
+
'- Run generic-ci validate and generic-ci render -o .gitlab-ci.yml after editing.\n'
|
|
178
|
+
'- Validate with your GitLab CI Lint and execute a test pipeline. Setup does not build, publish, provision or deploy anything.\n')
|
|
179
|
+
files = sorted(p for p in stage.rglob('*') if p.is_file())
|
|
180
|
+
for file in files:
|
|
181
|
+
dest = root / file.relative_to(stage)
|
|
182
|
+
if dest.exists() or dest.is_symlink() or any(p.is_symlink() for p in dest.parents):
|
|
183
|
+
raise ValueError(f'setup will not overwrite or follow symlinks: {dest}')
|
|
184
|
+
if any(p.exists() and not p.is_dir() for p in dest.parents):
|
|
185
|
+
raise ValueError(f'parent is not a directory: {dest}')
|
|
186
|
+
print('Proposed files:')
|
|
187
|
+
for file in files:
|
|
188
|
+
print(' ' + file.relative_to(stage).as_posix())
|
|
189
|
+
print('\nDelivery configuration:\n' + (stage / descriptor.get('delivery', 'delivery.yml')).read_text())
|
|
190
|
+
if args.dry_run:
|
|
191
|
+
print('Dry run: no project files written.')
|
|
192
|
+
return 0
|
|
193
|
+
if interactive and input('Write these files? [y/N]: ').strip().lower() not in ['y', 'yes']:
|
|
194
|
+
print('Cancelled: no project files written.')
|
|
195
|
+
return 0
|
|
196
|
+
created = []
|
|
197
|
+
try:
|
|
198
|
+
for file in files:
|
|
199
|
+
dest = root / file.relative_to(stage)
|
|
200
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
with dest.open('xb') as stream:
|
|
202
|
+
created.append(dest)
|
|
203
|
+
stream.write(file.read_bytes())
|
|
204
|
+
except OSError:
|
|
205
|
+
for file in created:
|
|
206
|
+
file.unlink(missing_ok=True)
|
|
207
|
+
raise
|
|
208
|
+
print('Setup complete. Review CI-SETUP.md, validate with GitLab CI Lint, and commit the configuration.')
|
|
209
|
+
return 0
|