diff --git a/app/actions/preview_projects/create.rb b/app/actions/preview_projects/create.rb new file mode 100644 index 00000000..87d9e3e4 --- /dev/null +++ b/app/actions/preview_projects/create.rb @@ -0,0 +1,27 @@ +class PreviewProjects::Create + extend LightService::Action + + expects :base_project, :pr + promises :preview_project + + executed do |context| + base_project = context.base_project + pr = context.pr + forked_project = base_project.dup + forked_project.branch = pr.branch + forked_project.name = "#{base_project.name}-#{pr.number}" + # Duplicate the project_credential_provider + new_project_credential_provider = base_project.project_credential_provider.dup + new_project_credential_provider.project = forked_project + + # Duplicate the services + base_project.services.map do |service| + new_service = service.dup + new_service.allow_public_networking = false + new_service.project = forked_project + end + + preview_project = PreviewProject.new(project: base_project) + context.preview_project = preview_project + end +end \ No newline at end of file diff --git a/app/avo/resources/preview_project.rb b/app/avo/resources/preview_project.rb new file mode 100644 index 00000000..51d50850 --- /dev/null +++ b/app/avo/resources/preview_project.rb @@ -0,0 +1,17 @@ +class Avo::Resources::PreviewProject < Avo::BaseResource + # self.includes = [] + # self.attachments = [] + # self.search = { + # query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) } + # } + + def fields + field :id, as: :id + field :project, as: :belongs_to + field :base_project, as: :belongs_to + field :external_id, as: :text + field :clean_up_command, as: :textarea + end +end + + diff --git a/app/components/form_field_component.html.erb b/app/components/form_field_component.html.erb index 49205a25..9df272fc 100644 --- a/app/components/form_field_component.html.erb +++ b/app/components/form_field_component.html.erb @@ -7,7 +7,9 @@
<%= @description %>
<% end %> -
+
+
<%= content %> +
\ No newline at end of file diff --git a/app/controllers/avo/preview_projects_controller.rb b/app/controllers/avo/preview_projects_controller.rb new file mode 100644 index 00000000..49adc21d --- /dev/null +++ b/app/controllers/avo/preview_projects_controller.rb @@ -0,0 +1,4 @@ +# This controller has been generated to enable Rails' resource routes. +# More information on https://docs.avohq.io/3.0/controllers.html +class Avo::PreviewProjectsController < Avo::ResourcesController +end diff --git a/app/controllers/projects/preview_projects_controller.rb b/app/controllers/projects/preview_projects_controller.rb new file mode 100644 index 00000000..d6bae9de --- /dev/null +++ b/app/controllers/projects/preview_projects_controller.rb @@ -0,0 +1,25 @@ +class Projects::PreviewProjectsController < Projects::BaseController + before_action :set_project + + class MockPr < Struct.new(:title, :number, :username) + end + + def index + @prs = [ + MockPr.new(title: "Support for preview apps", number: "717", username: "czhu12"), + MockPr.new(title: "Add gitlab support for non authenticated users", number: "710", username: "czhu12"), + ] + @running_projects = [ + PreviewProject.new(project:) + ] + end + + def create + end + + def edit + end + + def update + end +end \ No newline at end of file diff --git a/app/models/preview_project.rb b/app/models/preview_project.rb new file mode 100644 index 00000000..e756f7e1 --- /dev/null +++ b/app/models/preview_project.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: preview_projects +# +# id :bigint not null, primary key +# clean_up_command :text +# created_at :datetime not null +# updated_at :datetime not null +# external_id :string +# project_id :bigint not null +# +# Indexes +# +# index_preview_projects_on_project_id (project_id) +# +# Foreign Keys +# +# fk_rails_... (project_id => projects.id) +# +class PreviewProject < ApplicationRecord + belongs_to :project + #belongs_to :base_project +end diff --git a/app/models/project.rb b/app/models/project.rb index 09e8d6d7..ca950b1c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -40,6 +40,10 @@ class Project < ApplicationRecord has_many :volumes, dependent: :destroy has_one :project_credential_provider, dependent: :destroy + + has_one :preview_project, dependent: :destroy + has_many :previews, class_name: "Project" + validates :name, presence: true, format: { with: /\A[a-z0-9-]+\z/, message: "must be lowercase, numbers, and hyphens only" } validates :repository_url, presence: true, diff --git a/app/services/review_app/definition.rb b/app/services/review_app/definition.rb new file mode 100644 index 00000000..9024431f --- /dev/null +++ b/app/services/review_app/definition.rb @@ -0,0 +1,21 @@ +class ReviewApp::Definition + def initialize(yaml_path, context = {}) + content = if yaml_path.end_with?('.erb') + require 'erb' + erb = ERB.new(File.read(yaml_path)) + context_binding = binding + context.each { |key, value| context_binding.local_variable_set(key, value) } + erb.result(context_binding) + else + File.read(yaml_path) + end + + @definition = YAML.load(content) + end + + def to_yaml + { + name: @project.name, + } + end +end \ No newline at end of file diff --git a/app/views/projects/_sidebar.html.erb b/app/views/projects/_sidebar.html.erb index c9717dda..e8ded365 100644 --- a/app/views/projects/_sidebar.html.erb +++ b/app/views/projects/_sidebar.html.erb @@ -29,6 +29,12 @@ <% end %> + <%= link_to project_preview_projects_path(project), class: "tab hover:bg-base-content/15 #{'tab-active' if current_page?(project_preview_projects_path(project))}" do %> +
+ Preview Apps + +
+ <% end %> <%= link_to edit_project_path(project), class: "tab hover:bg-base-content/15 #{'tab-active' if current_page?(edit_project_path(project))}" do %>
Settings diff --git a/app/views/projects/edit.html.erb b/app/views/projects/edit.html.erb index e96844fb..f2d14d1c 100644 --- a/app/views/projects/edit.html.erb +++ b/app/views/projects/edit.html.erb @@ -7,6 +7,13 @@ <%= render "edit_form", project: @project %>
+
+

Preview Apps

+
+ + <%= render "projects/preview_apps/form", project: @project %> +
+

Volumes


diff --git a/app/views/projects/preview_projects/_form.html.erb b/app/views/projects/preview_projects/_form.html.erb new file mode 100644 index 00000000..9fd63ef7 --- /dev/null +++ b/app/views/projects/preview_projects/_form.html.erb @@ -0,0 +1,22 @@ +<%= form_with(model: project) do |form| %> + <%= render "shared/error_messages", resource: form.object %> +
+
+ Volumes are a way to persist data across container restarts. In general, you should use a database for this. + A volume will not work if you have more than one node in your cluster. +
+ <%= render(FormFieldComponent.new(label: "Enable preview apps")) do %> + <%= form.check_box :preview_apps, class: "checkbox" %> + <% end %> + + <%= render(FormFieldComponent.new( + label: "Cluster", + description: "The cluster to deploy the preview apps to." + )) do %> + <%= form.collection_select :cluster_id, current_account.clusters, :id, :name, { include_blank: "Select a cluster" }, { class: "select select-bordered w-full" } %> + + <% end %> +
+<% end %> \ No newline at end of file diff --git a/app/views/projects/preview_projects/_show.html.erb b/app/views/projects/preview_projects/_show.html.erb new file mode 100644 index 00000000..1668b211 --- /dev/null +++ b/app/views/projects/preview_projects/_show.html.erb @@ -0,0 +1,4 @@ +
+ <%= pr.number %> - <%= pr.title %> + <%= button_to preview %> +
\ No newline at end of file diff --git a/app/views/projects/preview_projects/index.html.erb b/app/views/projects/preview_projects/index.html.erb new file mode 100644 index 00000000..c6543d9c --- /dev/null +++ b/app/views/projects/preview_projects/index.html.erb @@ -0,0 +1,15 @@ +<%= content_for :title, t("scaffold.edit.title", model: "Project") %> + +<%= project_layout(@project) do %> +
+

Preview Apps

+
+
+ <% @prs.each do |pr| %> + <%= render "projects/preview_projects/show", pr: %> + <% end %> + + <% @created_projects.each do |pr| %> + <%= render "projects/preview_projects/show", pr: %> + <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 87bda37d..9fc41a71 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,6 +49,7 @@ Rails.application.routes.draw do collection do get "/:project_id/deployments", to: "projects/deployments#index", as: :root end + resources :preview_projects, only: %i[index edit create update], module: :projects resources :volumes, only: %i[index new create destroy], module: :projects resources :processes, only: %i[index show create destroy], module: :projects resources :services, only: %i[index new create destroy update], module: :projects do diff --git a/db/migrate/20250625164120_create_preview_projects.rb b/db/migrate/20250625164120_create_preview_projects.rb new file mode 100644 index 00000000..544108bc --- /dev/null +++ b/db/migrate/20250625164120_create_preview_projects.rb @@ -0,0 +1,13 @@ +class CreatePreviewProjects < ActiveRecord::Migration[7.2] + def change + create_table :preview_projects do |t| + t.references :project, null: false, foreign_key: true + #t.references :base_project, null: false, foreign_key: true + t.string :external_id + t.text :clean_up_command + + t.timestamps + end + add_index :preview_projects, :project_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 438f85bc..1ed97c12 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_04_15_222407) do +ActiveRecord::Schema[7.2].define(version: 2025_06_25_164120) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -212,6 +212,15 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_15_222407) do t.index ["recipient_type", "recipient_id"], name: "index_noticed_notifications_on_recipient" end + create_table "preview_projects", force: :cascade do |t| + t.bigint "project_id", null: false + t.string "external_id" + t.text "clean_up_command" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["project_id"], name: "index_preview_projects_on_project_id" + end + create_table "project_add_ons", force: :cascade do |t| t.bigint "project_id", null: false t.bigint "add_on_id", null: false @@ -321,6 +330,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_15_222407) do add_foreign_key "cron_schedules", "services" add_foreign_key "deployments", "builds" add_foreign_key "environment_variables", "projects" + add_foreign_key "preview_projects", "projects" add_foreign_key "project_add_ons", "add_ons" add_foreign_key "project_add_ons", "projects" add_foreign_key "project_credential_providers", "projects" diff --git a/spec/actions/preview_projects/create_spec.rb b/spec/actions/preview_projects/create_spec.rb new file mode 100644 index 00000000..04ac3f7b --- /dev/null +++ b/spec/actions/preview_projects/create_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +RSpec.describe PreviewProjects::Create do + class MockPr < Struct.new(:title, :number, :branch) + end + let(:base_project) { create(:project) } + let!(:service_1) { create(:service, project: base_project) } + let!(:service_2) { create(:service, project: base_project) } + let(:pr) { MockPr.new("Fake title", number: "7301", branch: "feature/test") } + + it "can create a preview project from a base project" do + result = described_class.execute(base_project:, pr:) + expect(result).to be_success + end +end diff --git a/spec/factories/preview_projects.rb b/spec/factories/preview_projects.rb new file mode 100644 index 00000000..605d02d0 --- /dev/null +++ b/spec/factories/preview_projects.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: preview_projects +# +# id :bigint not null, primary key +# clean_up_command :text +# created_at :datetime not null +# updated_at :datetime not null +# external_id :string +# project_id :bigint not null +# +# Indexes +# +# index_preview_projects_on_project_id (project_id) +# +# Foreign Keys +# +# fk_rails_... (project_id => projects.id) +# +FactoryBot.define do + factory :preview_project do + project { nil } + base_project { nil } + external_id { "MyString" } + clean_up_command { "MyText" } + end +end diff --git a/spec/models/preview_project_spec.rb b/spec/models/preview_project_spec.rb new file mode 100644 index 00000000..114de25b --- /dev/null +++ b/spec/models/preview_project_spec.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: preview_projects +# +# id :bigint not null, primary key +# clean_up_command :text +# created_at :datetime not null +# updated_at :datetime not null +# external_id :string +# project_id :bigint not null +# +# Indexes +# +# index_preview_projects_on_project_id (project_id) +# +# Foreign Keys +# +# fk_rails_... (project_id => projects.id) +# +require 'rails_helper' + +RSpec.describe PreviewProject, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end