forge-ci 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +273 -0
- data/exe/forge-ci +8 -0
- data/lib/forge_ci/cli.rb +203 -0
- data/lib/forge_ci/compiler.rb +537 -0
- data/lib/forge_ci/dag.rb +68 -0
- data/lib/forge_ci/errors.rb +101 -0
- data/lib/forge_ci/interpolator.rb +137 -0
- data/lib/forge_ci/ir.rb +117 -0
- data/lib/forge_ci/validator.rb +403 -0
- data/lib/forge_ci.rb +20 -0
- metadata +54 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b200819f98e6f303864df60059e2df4145cc5f20811db8e7111d3fc75781c540
|
|
4
|
+
data.tar.gz: c42f8826e29c17e82bcfb27813d03f20214c6b47cfb1e9069a516efc739dead6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 469f6da6b783565507019cb118749c74dcc5bd0cec712882211e0663417cb172a61ff6e7d4547546d7713ec800ac5abb84b1c9e87acdae0e75a3276e603d268b
|
|
7
|
+
data.tar.gz: fc6b483f9b7f780fa69a96bb3c5d9569d6c08cc0134a0ce5eb0176ac535346763257f00c8558358691ce61646c01f6cc2d0ab054e532fd286f5fd64c07a56d31
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Azmi Muwahid
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# forge-ci
|
|
2
|
+
|
|
3
|
+
[](https://github.com/azmi2409/forge-ci/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
An open-source, **CircleCI-compatible CI/CD system** — pure Ruby, MIT licensed.
|
|
6
|
+
|
|
7
|
+
**Status: MVP complete.** All 8 planned components are built, tested (259 runs,
|
|
8
|
+
1023 assertions, green), and running from this repo.
|
|
9
|
+
|
|
10
|
+
Push code to GitHub → webhook → config compiled → jobs claimed by polling
|
|
11
|
+
runners → executed in Docker → results and commit statuses posted back. No
|
|
12
|
+
inbound ports on runners, no proprietary control plane.
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
**1. Validate a config anywhere (Ruby ≥ 3.0 only, no Docker):**
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
gem build forge-ci.gemspec && gem install ./forge-ci-0.1.0.gem
|
|
20
|
+
forge-ci validate .circleci/config.yml
|
|
21
|
+
forge-ci compile .circleci/config.yml --branch main
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**2. Run the full system locally** (needs Docker for the runner):
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# Control plane (SQLite fallback for dev; Postgres for production)
|
|
28
|
+
cd web
|
|
29
|
+
DATABASE_URL="sqlite3:db/development.sqlite3" bin/rails db:prepare db:seed
|
|
30
|
+
DATABASE_URL="sqlite3:db/development.sqlite3" bin/rails server
|
|
31
|
+
# → http://localhost:3000 (admin / password in dev)
|
|
32
|
+
|
|
33
|
+
# Runner (separate shell; needs Docker Engine)
|
|
34
|
+
cd ../runner
|
|
35
|
+
CONTROL_PLANE_URL="http://localhost:3000" \
|
|
36
|
+
RUNNER_TOKEN="<raw token printed by db:seed>" \
|
|
37
|
+
bin/forge-ci-runner
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then create a pipeline: compile a config with the CLI and `POST` the IR to
|
|
41
|
+
`/api/v1/projects/:id/pipelines` (see `web/README.md` — API reference), or
|
|
42
|
+
install the GitHub App and just push.
|
|
43
|
+
|
|
44
|
+
Production deployment: **[DEPLOY.md](DEPLOY.md)** · Remaining work:
|
|
45
|
+
**[TODO.md](TODO.md)**
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
GitHub ──push/PR webhook──▶ ┌──────────────────┐
|
|
51
|
+
│ Control plane │──claim API──▶ ┌────────────┐
|
|
52
|
+
│ (Rails 8) │◀─heartbeats── │ Runner │
|
|
53
|
+
│ │──log chunks──▶│ (Docker) │
|
|
54
|
+
│ Postgres │ └────────────┘
|
|
55
|
+
│ S3/MinIO │ ┌────────────┐
|
|
56
|
+
└──────────────────┘◀─heartbeats── │ Runner │
|
|
57
|
+
│ │ (Docker) │
|
|
58
|
+
▼ └────────────┘
|
|
59
|
+
commit statuses
|
|
60
|
+
back to GitHub
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **Control plane** owns projects, pipelines, workflow/job state machines,
|
|
64
|
+
the GitHub App, and the polling APIs. Runners never accept inbound
|
|
65
|
+
connections — they poll and claim work, Buildkite-style.
|
|
66
|
+
- **Runners** are dumb and disposable: claim → `docker run` → stream logs →
|
|
67
|
+
report. Scale by adding hosts.
|
|
68
|
+
- **Config compatibility**: `.circleci/config.yml` (v2.1 subset) is compiled
|
|
69
|
+
server-side into a normalized IR, mirroring CircleCI's own
|
|
70
|
+
`configs/compile` semantics. See the supported-subset table below.
|
|
71
|
+
|
|
72
|
+
## Repository layout
|
|
73
|
+
|
|
74
|
+
| Path | What | Component |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `/` (this dir) | `forge-ci` gem: config **compiler** + CLI | 1 |
|
|
77
|
+
| `web/` | Rails control plane: intake, claim API, GitHub App, log streaming, cache/artifacts, contexts | 2, 3, 4, 6, 7, 8 |
|
|
78
|
+
| `runner/` | Docker runner daemon (stdlib-only Ruby) | 5 |
|
|
79
|
+
| `TODO.md` | Remaining work, honestly listed | — |
|
|
80
|
+
| `DEPLOY.md` | Production deployment guide | — |
|
|
81
|
+
|
|
82
|
+
Each part has its own README with setup, API, and env vars:
|
|
83
|
+
[`web/README.md`](web/README.md) · [`runner/README.md`](runner/README.md)
|
|
84
|
+
|
|
85
|
+
## Component status
|
|
86
|
+
|
|
87
|
+
| # | Component | Status |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| 1 | Config compiler (pure function: parse → validate → interpolate → DAG → IR) | ✅ shipped as the `forge-ci` gem |
|
|
90
|
+
| 2 | Rails control plane (projects, pipelines, state machines, REST API, web UI) | ✅ |
|
|
91
|
+
| 3 | GitHub App (HMAC webhooks, config fetch at SHA, commit statuses, fork-PR secret gating) | ✅ code done — [app registration is manual](web/README.md#github-app-setup-component-3) |
|
|
92
|
+
| 4 | Polling runner claim API (atomic claim, heartbeats, reclaim-on-timeout) | ✅ |
|
|
93
|
+
| 5 | Docker runner (poll → `docker run` + sidecars → step exec → report) | ✅ |
|
|
94
|
+
| 6 | Log streaming (chunked upload → object storage, SSE live tail) | ✅ |
|
|
95
|
+
| 7 | Cache + artifacts (S3-compatible, signed URLs, key fallback) | ✅ |
|
|
96
|
+
| 8 | Contexts/secrets (encrypted at rest, env injection, masking, rotation, fork gating) | ✅ |
|
|
97
|
+
|
|
98
|
+
Deliberately deferred: `machine`/`macos`/`windows` executors, orb registry,
|
|
99
|
+
dynamic config, `parallelism`/test splitting, cron pipelines, SSH re-run.
|
|
100
|
+
Full list in [TODO.md](TODO.md).
|
|
101
|
+
|
|
102
|
+
## The gem (config compiler)
|
|
103
|
+
|
|
104
|
+
`forge-ci` compiles CircleCI v2.1-style pipeline YAML into a normalized,
|
|
105
|
+
JSON-serializable **intermediate representation (IR)** that the control plane
|
|
106
|
+
and runner execute. Pure Ruby, **stdlib only** — no network, no processes, no
|
|
107
|
+
dependencies. Tested on Ruby 3.2.3+ (requires >= 3.0).
|
|
108
|
+
|
|
109
|
+
### Install
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# Gemfile
|
|
113
|
+
gem "forge-ci"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Or build locally:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
gem build forge-ci.gemspec
|
|
120
|
+
gem install ./forge-ci-0.1.0.gem
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Usage
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
require "forge_ci"
|
|
127
|
+
|
|
128
|
+
yaml = File.read(".circleci/config.yml")
|
|
129
|
+
|
|
130
|
+
ir, errors = ForgeCI::Compiler.compile(
|
|
131
|
+
yaml,
|
|
132
|
+
pipeline_params: {
|
|
133
|
+
"branch" => "main", # resolves << pipeline.git.branch >> and branch filters
|
|
134
|
+
"number" => 42, # resolves << pipeline.number >>
|
|
135
|
+
"parameters" => { "deploy_env" => "production" } # overrides for top-level `parameters:`
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if errors.empty?
|
|
140
|
+
puts JSON.pretty_generate(ir)
|
|
141
|
+
else
|
|
142
|
+
puts errors # actionable messages with YAML line numbers
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Just validation, no IR:
|
|
146
|
+
errors = ForgeCI::Compiler.validate(yaml)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### CLI
|
|
150
|
+
|
|
151
|
+
The gem ships a `forge-ci` executable (pure stdlib, no extra gems):
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
# Validate a config: exit 0 if valid, 1 otherwise.
|
|
155
|
+
forge-ci validate .circleci/config.yml
|
|
156
|
+
|
|
157
|
+
# Compile a config to its IR as JSON (pretty-printed by default).
|
|
158
|
+
forge-ci compile .circleci/config.yml --branch main --sha abc123 --number 42
|
|
159
|
+
|
|
160
|
+
# Compact JSON output:
|
|
161
|
+
forge-ci compile .circleci/config.yml --json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
On failure, each error goes to stderr with its YAML line number:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
$ forge-ci validate broken.yml
|
|
168
|
+
forge-ci: broken.yml: line 17: workflows.main.jobs: dependency cycle detected: a -> b -> a
|
|
169
|
+
$ echo $?
|
|
170
|
+
1
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`compile` returns `[ir, errors]`. `errors` is empty on success; `ir` is `nil`
|
|
174
|
+
when errors are present. `ForgeCI::Error` is raised **only** when the YAML text
|
|
175
|
+
cannot be parsed at all — everything else (unknown keys, bad references,
|
|
176
|
+
dependency cycles) comes back as error strings.
|
|
177
|
+
|
|
178
|
+
### IR shape
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
{
|
|
182
|
+
version: "2.1",
|
|
183
|
+
workflows: [
|
|
184
|
+
{
|
|
185
|
+
name: "build-test-deploy",
|
|
186
|
+
jobs: [
|
|
187
|
+
{
|
|
188
|
+
name: "build",
|
|
189
|
+
executor: {
|
|
190
|
+
type: "docker",
|
|
191
|
+
image: "cimg/node:20",
|
|
192
|
+
environment: { "NODE_ENV" => "test" },
|
|
193
|
+
secondary_images: [{ image: "redis:7", name: "redis", environment: {} }],
|
|
194
|
+
resource_class: "medium"
|
|
195
|
+
},
|
|
196
|
+
working_directory: "~/project",
|
|
197
|
+
steps: [
|
|
198
|
+
{ type: "checkout" },
|
|
199
|
+
{ type: "run", name: "Build", command: "npm run build",
|
|
200
|
+
shell: nil, background: false, when: "on_success" },
|
|
201
|
+
{ type: "save_cache", keys: ["node-deps-v1-abc"], paths: ["node_modules"] },
|
|
202
|
+
# ...
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
meta: { warnings: ["no branch supplied via pipeline_params; ..."] }
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Jobs inside each workflow are emitted in **topological order** of their
|
|
213
|
+
`requires` edges.
|
|
214
|
+
|
|
215
|
+
### Supported subset vs CircleCI
|
|
216
|
+
|
|
217
|
+
| CircleCI v2.1 feature | forge-ci v1 |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `version: 2.1` | ✅ |
|
|
220
|
+
| `parameters:` (string/boolean/integer/enum, defaults, overrides) | ✅ |
|
|
221
|
+
| `commands:` reusable steps with parameters | ✅ (inlined at compile time) |
|
|
222
|
+
| `executors:` reusable executors with parameters | ✅ (inlined; job-level keys win) |
|
|
223
|
+
| `jobs:` with `docker:` executor (primary + service images) | ✅ |
|
|
224
|
+
| `working_directory`, `environment`, `resource_class` | ✅ |
|
|
225
|
+
| Steps: `checkout`, `run`, `save_cache`, `restore_cache`, `persist_to_workspace`, `attach_workspace`, `store_artifacts`, `store_test_results`, `add_ssh_keys` | ✅ (last two: runner skips loudly, see below) |
|
|
226
|
+
| `run:` shorthand, `when: on_success/on_fail/always` | ✅ |
|
|
227
|
+
| `when:`/`unless:` conditional steps | ✅ (evaluated at compile time) |
|
|
228
|
+
| `<< parameters.x >>`, `<< pipeline.git.branch >>`, `<< pipeline.number >>` | ✅ |
|
|
229
|
+
| Workflows: `requires` DAG, cycle detection, topological order | ✅ |
|
|
230
|
+
| `filters: branches: only/ignore` (plain names and `/regex/`) | ✅ |
|
|
231
|
+
| `matrix:` (desugared to `job-0`, `job-1`, …; alias usable in `requires`) | ✅ |
|
|
232
|
+
| `orbs:` | ❌ rejected with a clear error (no registry in v1) |
|
|
233
|
+
| `machine:` / `macos:` executors | ❌ rejected with a clear error (Docker only) |
|
|
234
|
+
| `type: approval` jobs | ❌ rejected with a clear error |
|
|
235
|
+
| Dynamic config (`setup:` workflows) | ❌ not yet supported |
|
|
236
|
+
| Docker layer caching, `parallelism` / test splitting | ❌ not yet supported |
|
|
237
|
+
| `filters: tags:` | ⚠️ parsed, ignored with a warning |
|
|
238
|
+
| Missing `workflows:` | ⚠️ implicit `default` workflow with a warning |
|
|
239
|
+
|
|
240
|
+
Cache `keys:` templates (e.g. `{{ checksum "package.json" }}`) are passed
|
|
241
|
+
through untouched — checksum expansion is a runner concern.
|
|
242
|
+
|
|
243
|
+
### Design notes
|
|
244
|
+
|
|
245
|
+
- **Compile is a pure function.** YAML text in, IR out. No I/O, no network,
|
|
246
|
+
no Docker — the whole thing is unit-testable offline.
|
|
247
|
+
- **Validation is compilation.** Like CircleCI's own `configs/compile`
|
|
248
|
+
endpoint, there is no separate "validate then run" — one pass collects every
|
|
249
|
+
structural problem with file locations.
|
|
250
|
+
- **Errors, not exceptions.** The only raise is `ForgeCI::Error` on
|
|
251
|
+
unparseable YAML. Error strings carry Psych line numbers where the AST can
|
|
252
|
+
locate the key.
|
|
253
|
+
- **Warnings vs errors.** Anything that changes what runs (unknown step,
|
|
254
|
+
cycle, bad reference) is an error. Anything informational lands in
|
|
255
|
+
`ir[:meta][:warnings]`.
|
|
256
|
+
|
|
257
|
+
## Development
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
rake test # gem suite, fixtures in test/fixtures/
|
|
261
|
+
cd web && bin/rails test
|
|
262
|
+
cd runner && rake test
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Contributing
|
|
266
|
+
|
|
267
|
+
PRs welcome. Keep the zero-dependency philosophy for the gem and the runner
|
|
268
|
+
(stdlib only). The control plane is a standard Rails 8 app. Please include
|
|
269
|
+
tests — every component ships with its suite green.
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT — see [MIT-LICENSE](MIT-LICENSE).
|
data/exe/forge-ci
ADDED
data/lib/forge_ci/cli.rb
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module ForgeCI
|
|
7
|
+
# Command-line interface for forge-ci.
|
|
8
|
+
#
|
|
9
|
+
# forge-ci validate <config.yml>
|
|
10
|
+
# forge-ci compile <config.yml> [--branch B] [--sha S] [--number N] [--json]
|
|
11
|
+
# forge-ci --help | --version
|
|
12
|
+
#
|
|
13
|
+
# Kept deliberately thin so it is unit-testable: +run+ takes argv and IO
|
|
14
|
+
# objects and returns an integer exit status instead of calling +exit+
|
|
15
|
+
# itself. exe/forge-ci is just a wrapper that passes ARGV through.
|
|
16
|
+
class CLI
|
|
17
|
+
USAGE = <<~USAGE.freeze
|
|
18
|
+
Usage: forge-ci <command> [options]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
validate <config.yml> validate a pipeline config
|
|
22
|
+
(exit 0 if valid, 1 otherwise)
|
|
23
|
+
compile <config.yml> [--branch B] [--sha S]
|
|
24
|
+
[--number N] [--json] compile a pipeline config to
|
|
25
|
+
its IR as JSON
|
|
26
|
+
(pretty JSON by default,
|
|
27
|
+
--json for compact)
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
-h, --help show this help and exit
|
|
31
|
+
-v, --version show the gem version and exit
|
|
32
|
+
USAGE
|
|
33
|
+
|
|
34
|
+
# Exit statuses:
|
|
35
|
+
# 0 - success (config valid / compile succeeded / help / version)
|
|
36
|
+
# 1 - the config is invalid, the file could not be read, or the YAML
|
|
37
|
+
# could not be parsed
|
|
38
|
+
# 2 - CLI usage error (unknown command, bad flags, missing arguments)
|
|
39
|
+
def self.run(argv, out: $stdout, err: $stderr)
|
|
40
|
+
new(out: out, err: err).run(argv.dup)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(out:, err:)
|
|
44
|
+
@out = out
|
|
45
|
+
@err = err
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def run(argv)
|
|
49
|
+
command = argv.shift
|
|
50
|
+
case command
|
|
51
|
+
when "validate" then validate_command(argv)
|
|
52
|
+
when "compile" then compile_command(argv)
|
|
53
|
+
when "-h", "--help", nil then help
|
|
54
|
+
when "-v", "--version" then version
|
|
55
|
+
else usage_error("unknown command #{command.inspect}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def help
|
|
62
|
+
@out.puts USAGE
|
|
63
|
+
0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def version
|
|
67
|
+
@out.puts ForgeCI::VERSION
|
|
68
|
+
0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def usage_error(message)
|
|
72
|
+
@err.puts "forge-ci: #{message}"
|
|
73
|
+
@err.puts
|
|
74
|
+
@err.puts USAGE
|
|
75
|
+
2
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def read_config(path)
|
|
79
|
+
File.read(path)
|
|
80
|
+
rescue Errno::ENOENT
|
|
81
|
+
@err.puts "forge-ci: #{path}: no such file"
|
|
82
|
+
nil
|
|
83
|
+
rescue SystemCallError, IOError => e
|
|
84
|
+
@err.puts "forge-ci: #{path}: could not read file (#{e.message})"
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Prints each error (they already carry YAML line numbers) to stderr.
|
|
89
|
+
# Returns the exit status: 0 when there are no errors, 1 otherwise.
|
|
90
|
+
def report_errors(path, errors)
|
|
91
|
+
if errors.empty?
|
|
92
|
+
@out.puts "forge-ci: #{path}: valid"
|
|
93
|
+
0
|
|
94
|
+
else
|
|
95
|
+
errors.each { |error| @err.puts "forge-ci: #{path}: #{error}" }
|
|
96
|
+
1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def with_parsed_yaml(path)
|
|
101
|
+
yaml = read_config(path)
|
|
102
|
+
return 1 if yaml.nil?
|
|
103
|
+
|
|
104
|
+
yield yaml
|
|
105
|
+
rescue ForgeCI::Error => e
|
|
106
|
+
# The only raise in the library: the YAML could not be parsed at all.
|
|
107
|
+
@err.puts "forge-ci: #{path}: #{e.message}"
|
|
108
|
+
1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# -- forge-ci validate <config.yml> ------------------------------------
|
|
112
|
+
|
|
113
|
+
def validate_command(argv)
|
|
114
|
+
parser = OptionParser.new do |opts|
|
|
115
|
+
opts.banner = "Usage: forge-ci validate <config.yml>"
|
|
116
|
+
opts.on("-h", "--help", "show this help and exit") do
|
|
117
|
+
@out.puts opts
|
|
118
|
+
return 0
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
parser.parse!(argv)
|
|
124
|
+
rescue OptionParser::ParseError => e
|
|
125
|
+
return usage_error(e.message)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
path = argv.shift
|
|
129
|
+
return usage_error("validate requires a <config.yml> argument") if path.nil?
|
|
130
|
+
return usage_error("validate takes exactly one <config.yml> argument") unless argv.empty?
|
|
131
|
+
|
|
132
|
+
with_parsed_yaml(path) do |yaml|
|
|
133
|
+
report_errors(path, ForgeCI::Compiler.validate(yaml))
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# -- forge-ci compile <config.yml> --------------------------------------
|
|
138
|
+
|
|
139
|
+
def compile_command(argv)
|
|
140
|
+
options = {}
|
|
141
|
+
parser = OptionParser.new do |opts|
|
|
142
|
+
opts.banner = "Usage: forge-ci compile <config.yml> " \
|
|
143
|
+
"[--branch B] [--sha S] [--number N] [--json]"
|
|
144
|
+
opts.on("--branch BRANCH", "pipeline branch; resolves " \
|
|
145
|
+
"<< pipeline.git.branch >> and branch filters") do |v|
|
|
146
|
+
options[:branch] = v
|
|
147
|
+
end
|
|
148
|
+
opts.on("--sha SHA", "pipeline git sha (forwarded to the compiler " \
|
|
149
|
+
"for pipeline context)") do |v|
|
|
150
|
+
options[:sha] = v
|
|
151
|
+
end
|
|
152
|
+
opts.on("--number NUMBER", "pipeline number; resolves " \
|
|
153
|
+
"<< pipeline.number >>") do |v|
|
|
154
|
+
options[:number] = v
|
|
155
|
+
end
|
|
156
|
+
opts.on("--json", "emit compact JSON instead of pretty-printed JSON") do
|
|
157
|
+
options[:json] = true
|
|
158
|
+
end
|
|
159
|
+
opts.on("-h", "--help", "show this help and exit") do
|
|
160
|
+
@out.puts opts
|
|
161
|
+
return 0
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
begin
|
|
166
|
+
parser.parse!(argv)
|
|
167
|
+
rescue OptionParser::ParseError => e
|
|
168
|
+
return usage_error(e.message)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
path = argv.shift
|
|
172
|
+
return usage_error("compile requires a <config.yml> argument") if path.nil?
|
|
173
|
+
return usage_error("compile takes exactly one <config.yml> argument") unless argv.empty?
|
|
174
|
+
|
|
175
|
+
pipeline_params = build_pipeline_params(options)
|
|
176
|
+
|
|
177
|
+
with_parsed_yaml(path) do |yaml|
|
|
178
|
+
ir, errors = ForgeCI::Compiler.compile(yaml, pipeline_params: pipeline_params)
|
|
179
|
+
if errors.any?
|
|
180
|
+
errors.each { |error| @err.puts "forge-ci: #{path}: #{error}" }
|
|
181
|
+
1
|
|
182
|
+
else
|
|
183
|
+
@out.puts(options[:json] ? JSON.generate(ir) : JSON.pretty_generate(ir))
|
|
184
|
+
0
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def build_pipeline_params(options)
|
|
190
|
+
params = {}
|
|
191
|
+
params["branch"] = options[:branch] if options[:branch]
|
|
192
|
+
params["sha"] = options[:sha] if options[:sha]
|
|
193
|
+
params["number"] = coerce_number(options[:number]) if options[:number]
|
|
194
|
+
params
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Pipeline numbers are integers; keep anything else as-is so the
|
|
198
|
+
# interpolator still substitutes it.
|
|
199
|
+
def coerce_number(value)
|
|
200
|
+
value.match?(/\A\d+\z/) ? Integer(value, 10) : value
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|