Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,29 @@ e este projeto segue [Semantic Versioning](https://semver.org/lang/pt-BR/).

## [Unreleased]

### Added

- `GitignoreGenerator` → `.gitignore` (sempre gerado, faz parte da base) e
`DockerignoreGenerator` → `.dockerignore` (parte do grupo `--no-docker`).
Antes, a gem não gerava nenhum dos dois: um `git add .` logo após o `init`
podia commitar `.env`, `coverage/`, `log/`, `tmp/` etc., e o `Dockerfile`
gerado fazia `COPY . .` sem filtro nenhum — segredos e até o histórico do
`.git` entravam nas camadas da imagem Docker. O `.clignore` já listava
esses padrões como sensíveis para assistentes de IA, mas nada impedia de
fato que fossem parar em `git`/imagem Docker. Novas specs:
`spec/generators/gitignore_generator_spec.rb` e
`spec/generators/dockerignore_generator_spec.rb`.

### Fixed

- **Segurança**: o estágio `production` do `Dockerfile.erb` rodava como
root (nenhum estágio tinha diretiva `USER`). Adicionado usuário
`app` não-root nesse estágio (`groupadd`/`useradd` + `COPY --chown`);
os estágios `builder`/`development` continuam como root, de propósito,
para não quebrar o bind mount de desenvolvimento (`docker-compose.yml`
monta `.:/app`, e um usuário não-root ali gera conflito de permissão
com o UID do host). Nova spec em
`spec/generators/dockerfile_generator_spec.rb`.
- **Segurança**: `ExampleDomainGenerator` (usado por `--example-domain`) aceitava
qualquer string como `entity_name` e a interpolava sem validação em caminhos
de arquivo (`lib/domain/#{entity_name}.rb` etc.), permitindo path traversal
Expand Down
6 changes: 5 additions & 1 deletion lib/agentic_dev_workflow/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
require_relative 'generators/claude_md_generator'
require_relative 'generators/roadmap_generator'
require_relative 'generators/clignore_generator'
require_relative 'generators/gitignore_generator'
require_relative 'generators/rspec_generator'
require_relative 'generators/rubocop_generator'
require_relative 'generators/dockerfile_generator'
require_relative 'generators/docker_compose_generator'
require_relative 'generators/dockerignore_generator'
require_relative 'generators/git_hub_actions_generator'
require_relative 'generators/example_domain_generator'
require_relative 'generators/observability_generator'
Expand Down Expand Up @@ -45,6 +47,7 @@ def base_generators(target_dir)
Generators::ClaudeMdGenerator.new(target_dir: target_dir),
Generators::RoadmapGenerator.new(target_dir: target_dir),
Generators::ClignoreGenerator.new(target_dir: target_dir),
Generators::GitignoreGenerator.new(target_dir: target_dir),
Generators::RspecGenerator.new(target_dir: target_dir),
Generators::RubocopGenerator.new(target_dir: target_dir, profile: options['profile'])
]
Expand All @@ -66,7 +69,8 @@ def example_domain_requested?
def docker_generators(target_dir)
[
Generators::DockerfileGenerator.new(target_dir: target_dir),
Generators::DockerComposeGenerator.new(target_dir: target_dir)
Generators::DockerComposeGenerator.new(target_dir: target_dir),
Generators::DockerignoreGenerator.new(target_dir: target_dir)
]
end

Expand Down
19 changes: 19 additions & 0 deletions lib/agentic_dev_workflow/generators/dockerignore_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require_relative 'base_generator'

module AgenticDevWorkflow
module Generators
# Gera o arquivo .dockerignore no diretório alvo, evitando que segredos
# (.env, *.pem, *.key) e o histórico do .git entrem no contexto de build
# e, por consequência, nas camadas da imagem gerada pelo Dockerfile. Não
# sobrescreve um .dockerignore existente.
class DockerignoreGenerator < BaseGenerator
TEMPLATE = load_template('dockerignore.erb')

def generate
write_file('.dockerignore', render(TEMPLATE))
end
end
end
end
19 changes: 19 additions & 0 deletions lib/agentic_dev_workflow/generators/gitignore_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require_relative 'base_generator'

module AgenticDevWorkflow
module Generators
# Gera o arquivo .gitignore no diretório alvo, cobrindo os mesmos
# padrões sensíveis listados em .clignore (segredos, credenciais) mais
# os artefatos padrão de um projeto Ruby. Não sobrescreve um .gitignore
# existente.
class GitignoreGenerator < BaseGenerator
TEMPLATE = load_template('gitignore.erb')

def generate
write_file('.gitignore', render(TEMPLATE))
end
end
end
end
4 changes: 3 additions & 1 deletion lib/agentic_dev_workflow/templates/Dockerfile.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ COPY . .
CMD ["bundle", "exec", "rspec"]

FROM ruby:${RUBY_VERSION}-slim AS production
RUN groupadd --system app && useradd --system --gid app --create-home --home-dir /app app
WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . .
COPY --chown=app:app . .
USER app
CMD ["bundle", "exec", "rackup"]
15 changes: 15 additions & 0 deletions lib/agentic_dev_workflow/templates/dockerignore.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Impede que segredos e o histórico do git entrem no contexto de build e,
# por consequência, nas camadas da imagem gerada pelo Dockerfile.
.git
.env
.env.*
*.pem
*.key
config/master.key
config/credentials/*.key

/coverage/
/log/
/tmp/
/.bundle/
*.gem
17 changes: 17 additions & 0 deletions lib/agentic_dev_workflow/templates/gitignore.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Segredos e credenciais — nunca commitar.
.env
.env.*
*.pem
*.key
config/master.key
config/credentials/*.key

# Dependências e artefatos gerados.
/vendor/
/node_modules/
/tmp/
/log/
/coverage/
/pkg/
/.bundle/
*.gem
9 changes: 9 additions & 0 deletions spec/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ def path_in_target(*parts)
expect(File.exist?(path_in_target('CLAUDE.md'))).to be true
expect(File.exist?(path_in_target('ROADMAP.md'))).to be true
expect(File.exist?(path_in_target('.clignore'))).to be true
expect(File.exist?(path_in_target('.gitignore'))).to be true
expect(File.exist?(path_in_target('.rspec'))).to be true
expect(File.exist?(path_in_target('spec', 'spec_helper.rb'))).to be true
expect(File.exist?(path_in_target('.rubocop.yml'))).to be true
expect(File.exist?(path_in_target('Dockerfile'))).to be true
expect(File.exist?(path_in_target('docker-compose.yml'))).to be true
expect(File.exist?(path_in_target('.dockerignore'))).to be true
expect(File.exist?(path_in_target('.github', 'workflows', 'ci.yml'))).to be true
expect(File.exist?(path_in_target('docker-compose.observability.yml'))).to be true
expect(File.exist?(path_in_target('observability', 'prometheus.yml'))).to be true
Expand All @@ -44,6 +46,13 @@ def path_in_target(*parts)

expect(File.exist?(path_in_target('Dockerfile'))).to be false
expect(File.exist?(path_in_target('docker-compose.yml'))).to be false
expect(File.exist?(path_in_target('.dockerignore'))).to be false
end

it 'gera .gitignore mesmo com --no-docker (não faz parte do grupo docker)' do
described_class.start(['init', target_dir, '--no-docker'])

expect(File.exist?(path_in_target('.gitignore'))).to be true
end

it 'não gera .github/workflows/ci.yml quando --no-ci' do
Expand Down
7 changes: 7 additions & 0 deletions spec/generators/dockerfile_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
expect(content).to include('AS production')
end

it 'roda o estágio de produção com um usuário não-root' do
generator.generate
production_stage = File.read(dockerfile_path).split('AS production').last

expect(production_stage).to include('USER app')
end

it 'não sobrescreve um Dockerfile já existente (idempotência)' do
File.write(dockerfile_path, 'conteúdo customizado pelo usuário')

Expand Down
49 changes: 49 additions & 0 deletions spec/generators/dockerignore_generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require 'spec_helper'
require 'fakefs/spec_helpers'
require 'agentic_dev_workflow/generators/dockerignore_generator'

RSpec.describe AgenticDevWorkflow::Generators::DockerignoreGenerator do
include FakeFS::SpecHelpers

let(:target_dir) { '/fake/project' }
let(:generator) { described_class.new(target_dir: target_dir) }
let(:dockerignore_path) { File.join(target_dir, '.dockerignore') }

before { FileUtils.mkdir_p(target_dir) }

describe '#generate' do
it 'cria o arquivo .dockerignore no diretório alvo' do
generator.generate

expect(File.exist?(dockerignore_path)).to be true
end

it 'exclui segredos e o histórico do git do contexto de build' do
generator.generate
content = File.read(dockerignore_path)

expect(content).to include('.git')
expect(content).to include('.env')
expect(content).to include('*.pem')
expect(content).to include('*.key')
expect(content).to include('config/master.key')
end

it 'não exclui spec/ (necessário para o estágio development rodar rspec no container)' do
generator.generate
content = File.read(dockerignore_path)

expect(content).not_to include('/spec/')
end

it 'não sobrescreve um .dockerignore já existente (idempotência)' do
File.write(dockerignore_path, 'conteúdo customizado pelo usuário')

generator.generate

expect(File.read(dockerignore_path)).to eq('conteúdo customizado pelo usuário')
end
end
end
51 changes: 51 additions & 0 deletions spec/generators/gitignore_generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require 'spec_helper'
require 'fakefs/spec_helpers'
require 'agentic_dev_workflow/generators/gitignore_generator'

RSpec.describe AgenticDevWorkflow::Generators::GitignoreGenerator do
include FakeFS::SpecHelpers

let(:target_dir) { '/fake/project' }
let(:generator) { described_class.new(target_dir: target_dir) }
let(:gitignore_path) { File.join(target_dir, '.gitignore') }

before { FileUtils.mkdir_p(target_dir) }

describe '#generate' do
it 'cria o arquivo .gitignore no diretório alvo' do
generator.generate

expect(File.exist?(gitignore_path)).to be true
end

it 'inclui os padrões de segredos e credenciais' do
generator.generate
content = File.read(gitignore_path)

expect(content).to include('.env')
expect(content).to include('*.pem')
expect(content).to include('*.key')
expect(content).to include('config/master.key')
end

it 'inclui os artefatos padrão de um projeto Ruby' do
generator.generate
content = File.read(gitignore_path)

expect(content).to include('/coverage/')
expect(content).to include('/tmp/')
expect(content).to include('/log/')
expect(content).to include('*.gem')
end

it 'não sobrescreve um .gitignore já existente (idempotência)' do
File.write(gitignore_path, 'conteúdo customizado pelo usuário')

generator.generate

expect(File.read(gitignore_path)).to eq('conteúdo customizado pelo usuário')
end
end
end
Loading