git_adr/cli/
ci.rs

1//! CI/CD workflow generation for ADR integration.
2
3use anyhow::Result;
4use clap::{Args as ClapArgs, Subcommand};
5use colored::Colorize;
6use std::fs;
7use std::path::Path;
8
9use crate::core::Git;
10
11/// Arguments for the CI command.
12#[derive(ClapArgs, Debug)]
13pub struct Args {
14    /// CI subcommand.
15    #[command(subcommand)]
16    pub command: CiCommand,
17}
18
19/// CI subcommands.
20#[derive(Subcommand, Debug)]
21pub enum CiCommand {
22    /// Generate GitHub Actions workflow.
23    Github(GithubArgs),
24
25    /// Generate GitLab CI configuration.
26    Gitlab(GitlabArgs),
27}
28
29/// Arguments for GitHub Actions generation.
30#[derive(ClapArgs, Debug)]
31pub struct GithubArgs {
32    /// Output directory.
33    #[arg(long, short, default_value = ".github/workflows")]
34    pub output: String,
35
36    /// Force overwrite existing files.
37    #[arg(long, short)]
38    pub force: bool,
39
40    /// Include ADR validation in PR checks.
41    #[arg(long, default_value = "true")]
42    pub validation: bool,
43
44    /// Include ADR sync on push.
45    #[arg(long, default_value = "true")]
46    pub sync: bool,
47}
48
49/// Arguments for GitLab CI generation.
50#[derive(ClapArgs, Debug)]
51pub struct GitlabArgs {
52    /// Output file path.
53    #[arg(long, short, default_value = ".gitlab-ci.yml")]
54    pub output: String,
55
56    /// Force overwrite existing file.
57    #[arg(long, short)]
58    pub force: bool,
59
60    /// Include ADR validation in pipeline.
61    #[arg(long, default_value = "true")]
62    pub validation: bool,
63
64    /// Include ADR sync in pipeline.
65    #[arg(long, default_value = "true")]
66    pub sync: bool,
67}
68
69/// Run the CI command.
70///
71/// # Errors
72///
73/// Returns an error if CI generation fails.
74pub fn run(args: Args) -> Result<()> {
75    let git = Git::new();
76    git.check_repository()?;
77
78    match args.command {
79        CiCommand::Github(github_args) => run_github(github_args),
80        CiCommand::Gitlab(gitlab_args) => run_gitlab(gitlab_args),
81    }
82}
83
84/// Generate GitHub Actions workflow.
85fn run_github(args: GithubArgs) -> Result<()> {
86    let output_dir = Path::new(&args.output);
87
88    if !output_dir.exists() {
89        fs::create_dir_all(output_dir)?;
90        eprintln!("{} Created {}", "✓".green(), args.output.cyan());
91    }
92
93    let workflow_path = output_dir.join("adr.yml");
94
95    if workflow_path.exists() && !args.force {
96        anyhow::bail!(
97            "Workflow file already exists: {}. Use --force to overwrite.",
98            workflow_path.display()
99        );
100    }
101
102    let workflow = generate_github_workflow(args.validation, args.sync);
103    fs::write(&workflow_path, workflow)?;
104
105    eprintln!(
106        "{} Generated GitHub Actions workflow: {}",
107        "✓".green(),
108        workflow_path.display().to_string().cyan()
109    );
110
111    eprintln!();
112    eprintln!("{} Workflow includes:", "→".blue());
113    if args.validation {
114        eprintln!("  • ADR validation on pull requests");
115    }
116    if args.sync {
117        eprintln!("  • ADR sync on push to main branch");
118    }
119
120    Ok(())
121}
122
123/// Generate GitLab CI configuration.
124fn run_gitlab(args: GitlabArgs) -> Result<()> {
125    let output_path = Path::new(&args.output);
126
127    if output_path.exists() && !args.force {
128        anyhow::bail!(
129            "GitLab CI file already exists: {}. Use --force to overwrite.",
130            output_path.display()
131        );
132    }
133
134    let config = generate_gitlab_ci(args.validation, args.sync);
135    fs::write(output_path, config)?;
136
137    eprintln!(
138        "{} Generated GitLab CI configuration: {}",
139        "✓".green(),
140        output_path.display().to_string().cyan()
141    );
142
143    eprintln!();
144    eprintln!("{} Configuration includes:", "→".blue());
145    if args.validation {
146        eprintln!("  • ADR validation job");
147    }
148    if args.sync {
149        eprintln!("  • ADR sync job");
150    }
151
152    Ok(())
153}
154
155/// Generate GitHub Actions workflow content.
156fn generate_github_workflow(validation: bool, sync: bool) -> String {
157    let mut workflow = String::new();
158
159    workflow.push_str(
160        r#"# ADR (Architecture Decision Records) CI/CD Workflow
161# Generated by git-adr
162
163name: ADR
164
165on:
166  push:
167    branches: [main, master]
168  pull_request:
169    branches: [main, master]
170
171env:
172  GIT_ADR_VERSION: "1.0.0"
173
174jobs:
175"#,
176    );
177
178    if validation {
179        workflow.push_str(
180            r#"
181  validate:
182    name: Validate ADRs
183    runs-on: ubuntu-latest
184    if: github.event_name == 'pull_request'
185    steps:
186      - uses: actions/checkout@v4
187        with:
188          fetch-depth: 0
189
190      - name: Install git-adr
191        run: |
192          curl -sSL https://github.com/zircote/git-adr/releases/download/v${{ env.GIT_ADR_VERSION }}/git-adr-x86_64-unknown-linux-gnu.tar.gz | tar xz
193          sudo mv git-adr /usr/local/bin/
194
195      - name: Fetch ADR notes
196        run: |
197          git fetch origin 'refs/notes/*:refs/notes/*' || true
198
199      - name: List ADRs
200        run: git-adr list
201
202      - name: Validate ADR references
203        run: |
204          # Check for ADR references in commit messages
205          COMMITS=$(git log --format="%s" origin/${{ github.base_ref }}..HEAD)
206          echo "Checking commits for ADR references..."
207          echo "$COMMITS" | grep -i "ADR-" && echo "✓ Found ADR references" || echo "→ No ADR references found"
208"#,
209        );
210    }
211
212    if sync {
213        workflow.push_str(
214            r#"
215  sync:
216    name: Sync ADRs
217    runs-on: ubuntu-latest
218    if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
219    steps:
220      - uses: actions/checkout@v4
221        with:
222          fetch-depth: 0
223          token: ${{ secrets.GITHUB_TOKEN }}
224
225      - name: Install git-adr
226        run: |
227          curl -sSL https://github.com/zircote/git-adr/releases/download/v${{ env.GIT_ADR_VERSION }}/git-adr-x86_64-unknown-linux-gnu.tar.gz | tar xz
228          sudo mv git-adr /usr/local/bin/
229
230      - name: Configure git
231        run: |
232          git config user.name "github-actions[bot]"
233          git config user.email "github-actions[bot]@users.noreply.github.com"
234
235      - name: Fetch ADR notes
236        run: |
237          git fetch origin 'refs/notes/*:refs/notes/*' || true
238
239      - name: Sync ADRs
240        run: |
241          git-adr sync || echo "→ No changes to sync"
242"#,
243        );
244    }
245
246    workflow
247}
248
249/// Generate GitLab CI content.
250fn generate_gitlab_ci(validation: bool, sync: bool) -> String {
251    let mut config = String::new();
252
253    config.push_str(
254        r#"# ADR (Architecture Decision Records) CI/CD Configuration
255# Generated by git-adr
256
257stages:
258  - validate
259  - sync
260
261variables:
262  GIT_ADR_VERSION: "1.0.0"
263
264.install-git-adr: &install-git-adr
265  - curl -sSL https://github.com/zircote/git-adr/releases/download/v${GIT_ADR_VERSION}/git-adr-x86_64-unknown-linux-gnu.tar.gz | tar xz
266  - mv git-adr /usr/local/bin/
267  - git fetch origin 'refs/notes/*:refs/notes/*' || true
268"#,
269    );
270
271    if validation {
272        config.push_str(
273            r#"
274
275validate-adrs:
276  stage: validate
277  image: ubuntu:latest
278  rules:
279    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
280  before_script:
281    - apt-get update && apt-get install -y curl git
282    - *install-git-adr
283  script:
284    - git-adr list
285    - |
286      echo "Checking for ADR references in commits..."
287      git log --format="%s" ${CI_MERGE_REQUEST_DIFF_BASE_SHA}..HEAD | grep -i "ADR-" && echo "✓ Found ADR references" || echo "→ No ADR references found"
288"#,
289        );
290    }
291
292    if sync {
293        config.push_str(
294            r#"
295
296sync-adrs:
297  stage: sync
298  image: ubuntu:latest
299  rules:
300    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
301  before_script:
302    - apt-get update && apt-get install -y curl git
303    - git config user.name "GitLab CI"
304    - git config user.email "ci@gitlab.com"
305    - *install-git-adr
306  script:
307    - git-adr sync || echo "→ No changes to sync"
308"#,
309        );
310    }
311
312    config
313}