merged
@@ -39,6 +39,9 @@ RUN apt-get update -qq && \
|
||||
RUN curl -fL https://app.getambassador.io/download/tel2oss/releases/download/v2.21.1/telepresence-linux-amd64 -o /usr/local/bin/telepresence && \
|
||||
chmod a+x /usr/local/bin/telepresence
|
||||
|
||||
# Install pack CLI for Cloud Native Buildpacks
|
||||
RUN curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.38.2/pack-v0.38.2-linux.tgz" | tar -xz -C /usr/local/bin
|
||||
|
||||
# Install application gems
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN bundle install && \
|
||||
@@ -87,6 +90,7 @@ COPY --from=build /rails /rails
|
||||
COPY --from=build /usr/local/bin/kubectl /usr/local/bin/kubectl
|
||||
COPY --from=build /usr/local/bin/helm /usr/local/bin/helm
|
||||
COPY --from=build /usr/local/bin/telepresence /usr/local/bin/telepresence
|
||||
COPY --from=build /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
# Entrypoint prepares the database.
|
||||
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
||||
|
||||
30
Gemfile.lock
@@ -101,7 +101,7 @@ GEM
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.3)
|
||||
avo (3.25.1)
|
||||
avo (3.25.3)
|
||||
actionview (>= 6.1)
|
||||
active_link_to
|
||||
activerecord (>= 6.1)
|
||||
@@ -120,7 +120,7 @@ GEM
|
||||
avo-heroicons (0.1.1)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.1)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (3.3.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.6)
|
||||
@@ -140,7 +140,7 @@ GEM
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.4)
|
||||
crack (1.0.0)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
@@ -152,7 +152,7 @@ GEM
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.4.1)
|
||||
date (3.5.0)
|
||||
debug (1.11.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
@@ -192,7 +192,7 @@ GEM
|
||||
dry-inflector (~> 1.0)
|
||||
dry-logic (~> 1.4)
|
||||
zeitwerk (~> 2.6)
|
||||
erb (5.1.1)
|
||||
erb (5.1.3)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
@@ -244,7 +244,7 @@ GEM
|
||||
fugit (>= 1.11.0)
|
||||
railties (>= 6.1.0)
|
||||
thor (>= 1.0.0)
|
||||
hashdiff (1.2.0)
|
||||
hashdiff (1.2.1)
|
||||
hashie (5.0.0)
|
||||
http (5.2.0)
|
||||
addressable (~> 2.8)
|
||||
@@ -322,8 +322,8 @@ GEM
|
||||
net-smtp
|
||||
marcel (1.1.0)
|
||||
matrix (0.4.2)
|
||||
meta-tags (2.22.1)
|
||||
actionpack (>= 6.0.0, < 8.1)
|
||||
meta-tags (2.22.2)
|
||||
actionpack (>= 6.0.0, < 8.2)
|
||||
method_source (1.1.0)
|
||||
mime-types (3.5.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
@@ -472,7 +472,7 @@ GEM
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.0)
|
||||
rake (13.3.1)
|
||||
rdoc (6.15.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
@@ -488,7 +488,7 @@ GEM
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
rexml (3.4.1)
|
||||
rexml (3.4.4)
|
||||
rspec-core (3.13.5)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
@@ -538,7 +538,7 @@ GEM
|
||||
ruby-vips (2.2.2)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
rubyzip (3.2.0)
|
||||
rubyzip (3.2.1)
|
||||
sanitize (6.1.3)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
@@ -599,7 +599,7 @@ GEM
|
||||
railties (>= 7.0.0)
|
||||
thor (1.4.0)
|
||||
tilt (2.4.0)
|
||||
timeout (0.4.3)
|
||||
timeout (0.4.4)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.17)
|
||||
actionpack (>= 7.1.0)
|
||||
@@ -614,8 +614,8 @@ GEM
|
||||
uri (1.0.3)
|
||||
useragent (0.16.11)
|
||||
version_gem (1.1.4)
|
||||
view_component (4.0.2)
|
||||
activesupport (>= 7.1.0, < 8.1)
|
||||
view_component (4.1.0)
|
||||
activesupport (>= 7.1.0, < 8.2)
|
||||
concurrent-ruby (~> 1)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
@@ -624,7 +624,7 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.25.1)
|
||||
webmock (3.26.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
||||
78
app/actions/buildpacks/details.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
class Buildpacks::Details
|
||||
extend LightService::Action
|
||||
expects :namespace, :name
|
||||
promises :result
|
||||
|
||||
# Struct for individual buildpack versions
|
||||
BuildpackVersion = Struct.new(
|
||||
:version,
|
||||
:link,
|
||||
keyword_init: true
|
||||
) do
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
version: hash["version"],
|
||||
link: hash["_link"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Struct for latest buildpack information (details endpoint has fewer fields)
|
||||
BuildpackLatestDetails = Struct.new(
|
||||
:version,
|
||||
:namespace,
|
||||
:name,
|
||||
:description,
|
||||
:homepage,
|
||||
:licenses,
|
||||
:stacks,
|
||||
:id,
|
||||
:verified,
|
||||
keyword_init: true
|
||||
) do
|
||||
VERIFIED_NAMESPACES = %w[io.buildpacks paketo-buildpacks heroku tanzu-buildpacks].freeze
|
||||
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
version: hash["version"],
|
||||
namespace: hash["namespace"],
|
||||
name: hash["name"],
|
||||
description: hash["description"],
|
||||
homepage: hash["homepage"],
|
||||
licenses: hash["licenses"],
|
||||
stacks: hash["stacks"],
|
||||
id: hash["id"],
|
||||
verified: VERIFIED_NAMESPACES.include?(hash["namespace"])
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Struct for buildpack details result
|
||||
BuildpackDetailsResult = Struct.new(
|
||||
:latest,
|
||||
:versions,
|
||||
keyword_init: true
|
||||
) do
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
latest: BuildpackLatestDetails.from_hash(hash["latest"]),
|
||||
versions: hash["versions"]&.map { |v| BuildpackVersion.from_hash(v) } || []
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
executed do |context|
|
||||
response = HTTParty.get(
|
||||
"https://registry.buildpacks.io/api/v1/buildpacks/#{context.namespace}/#{context.name}",
|
||||
headers: {
|
||||
"Accept" => "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
if response.success?
|
||||
context.result = BuildpackDetailsResult.from_hash(response.parsed_response)
|
||||
else
|
||||
context.fail_and_return!("Failed to fetch buildpack details: #{response.code}: #{response.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
94
app/actions/buildpacks/search.rb
Normal file
@@ -0,0 +1,94 @@
|
||||
class Buildpacks::Search
|
||||
extend LightService::Action
|
||||
expects :query
|
||||
promises :results
|
||||
|
||||
# Struct for individual buildpack versions
|
||||
BuildpackVersion = Struct.new(
|
||||
:version,
|
||||
:link,
|
||||
keyword_init: true
|
||||
) do
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
version: hash["version"],
|
||||
link: hash["_link"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Struct for latest buildpack information
|
||||
BuildpackLatest = Struct.new(
|
||||
:id,
|
||||
:namespace,
|
||||
:name,
|
||||
:version,
|
||||
:addr,
|
||||
:yanked,
|
||||
:description,
|
||||
:homepage,
|
||||
:licenses,
|
||||
:stacks,
|
||||
:created_at,
|
||||
:updated_at,
|
||||
:version_major,
|
||||
:version_minor,
|
||||
:version_patch,
|
||||
:verified,
|
||||
keyword_init: true
|
||||
) do
|
||||
VERIFIED_NAMESPACES = %w[io.buildpacks paketo-buildpacks heroku tanzu-buildpacks].freeze
|
||||
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
id: hash["id"],
|
||||
namespace: hash["namespace"],
|
||||
name: hash["name"],
|
||||
version: hash["version"],
|
||||
addr: hash["addr"],
|
||||
yanked: hash["yanked"],
|
||||
description: hash["description"],
|
||||
homepage: hash["homepage"],
|
||||
licenses: hash["licenses"],
|
||||
stacks: hash["stacks"],
|
||||
created_at: hash["created_at"],
|
||||
updated_at: hash["updated_at"],
|
||||
version_major: hash["version_major"],
|
||||
version_minor: hash["version_minor"],
|
||||
version_patch: hash["version_patch"],
|
||||
verified: VERIFIED_NAMESPACES.include?(hash["namespace"])
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Struct for complete buildpack search result
|
||||
BuildpackResult = Struct.new(
|
||||
:latest,
|
||||
:versions,
|
||||
keyword_init: true
|
||||
) do
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
latest: BuildpackLatest.from_hash(hash["latest"]),
|
||||
versions: hash["versions"]&.map { |v| BuildpackVersion.from_hash(v) } || []
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
executed do |context|
|
||||
response = HTTParty.get(
|
||||
"https://registry.buildpacks.io/api/v1/search",
|
||||
query: { matches: context.query },
|
||||
headers: {
|
||||
"Accept" => "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
if response.success?
|
||||
parsed_results = response.parsed_response.map { |result| BuildpackResult.from_hash(result) }
|
||||
context.results = parsed_results
|
||||
else
|
||||
context.fail_and_return!("Failed to search buildpacks: #{response.code}: #{response.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,8 +9,6 @@ module Projects
|
||||
:repository_url,
|
||||
:branch,
|
||||
:cluster_id,
|
||||
:docker_build_context_directory,
|
||||
:dockerfile_path,
|
||||
:container_registry_url,
|
||||
:predeploy_command,
|
||||
:project_fork_status,
|
||||
@@ -57,7 +55,10 @@ module Projects
|
||||
{
|
||||
provider: project.project_credential_provider.provider,
|
||||
driver: BuildConfiguration::DEFAULT_BUILDER,
|
||||
image_repository: project.repository_url
|
||||
build_type: :dockerfile,
|
||||
image_repository: project.repository_url,
|
||||
context_directory: ".",
|
||||
dockerfile_path: "./Dockerfile"
|
||||
}
|
||||
end
|
||||
|
||||
@@ -68,6 +69,7 @@ module Projects
|
||||
end
|
||||
|
||||
steps << Projects::ValidateNamespaceAvailability
|
||||
steps << Projects::InitializeBuildPacks
|
||||
steps << Projects::Save
|
||||
|
||||
# Only register webhook in cloud mode
|
||||
|
||||
35
app/actions/projects/initialize_build_packs.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
class InitializeBuildPacks
|
||||
extend LightService::Action
|
||||
|
||||
expects :build_configuration, :params
|
||||
promises :build_packs
|
||||
|
||||
def self.fetch_buildpack_details!(build_pack)
|
||||
result = Buildpacks::Details.execute(
|
||||
namespace: build_pack.namespace,
|
||||
name: build_pack.name
|
||||
)
|
||||
build_pack.details = result.result.to_h
|
||||
build_pack
|
||||
end
|
||||
|
||||
executed do |context|
|
||||
context.build_packs = []
|
||||
build_configuration = context.build_configuration
|
||||
next context unless build_configuration&.buildpacks?
|
||||
|
||||
build_packs_params = context.params
|
||||
.dig(:project, :build_configuration, :build_packs_attributes) || []
|
||||
next context unless build_packs_params
|
||||
|
||||
context.build_packs = build_packs_params.map.with_index do |pack_params, build_order|
|
||||
permitted = pack_params.permit(:namespace, :name, :version, :reference_type)
|
||||
build_pack = build_configuration.build_packs.build(permitted.merge(build_order:))
|
||||
fetch_buildpack_details!(build_pack)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,8 @@ module Projects
|
||||
build_configuration:,
|
||||
params:
|
||||
).reduce(
|
||||
Projects::UpdateSave
|
||||
Projects::UpdateSave,
|
||||
Projects::UpdateBuildPacks
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
55
app/actions/projects/update_build_packs.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
class Projects::UpdateBuildPacks
|
||||
extend LightService::Action
|
||||
|
||||
expects :build_configuration, :params
|
||||
|
||||
executed do |context|
|
||||
build_configuration = context.build_configuration
|
||||
next context unless build_configuration&.buildpacks?
|
||||
|
||||
build_packs_params = context.params
|
||||
.dig(:project, :build_configuration, :build_packs_attributes) || []
|
||||
|
||||
# Create a hash of existing build packs keyed by build_pack.key
|
||||
existing_packs = {}
|
||||
build_configuration.build_packs.each do |pack|
|
||||
existing_packs[pack.key] = pack
|
||||
end
|
||||
|
||||
# Track which build packs are in the params and their order
|
||||
incoming_keys = []
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
# Process each incoming build pack
|
||||
build_packs_params.each_with_index do |pack_params, build_order|
|
||||
permitted = pack_params.permit(:namespace, :name, :version, :reference_type)
|
||||
namespace = permitted[:namespace]
|
||||
name = permitted[:name]
|
||||
|
||||
key = "#{namespace}/#{name}"
|
||||
next if namespace.blank? || name.blank?
|
||||
|
||||
incoming_keys << key unless incoming_keys.include?(key)
|
||||
|
||||
if existing_packs[key]
|
||||
# Build pack already exists, update its order
|
||||
build_pack = existing_packs[key]
|
||||
build_pack.build_order = build_order
|
||||
else
|
||||
# Build pack doesn't exist, create it and fetch details
|
||||
build_pack = build_configuration.build_packs.build(permitted.merge(build_order:))
|
||||
Projects::InitializeBuildPacks.fetch_buildpack_details!(build_pack)
|
||||
end
|
||||
build_pack.save!
|
||||
end
|
||||
|
||||
# Delete build packs that are not in the incoming params (only persisted ones)
|
||||
packs_to_delete = build_configuration.build_packs.reject do |pack|
|
||||
incoming_keys.include?(pack.key)
|
||||
end
|
||||
packs_to_delete.each(&:destroy!)
|
||||
end
|
||||
|
||||
context
|
||||
end
|
||||
end
|
||||
16
app/avo/resources/build_pack.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class Avo::Resources::BuildPack < Avo::BaseResource
|
||||
# self.includes = []
|
||||
# self.attachments = []
|
||||
# self.search = {
|
||||
# query: -> { query.ransack(id_eq: q, m: "or").result(distinct: false) }
|
||||
# }
|
||||
|
||||
def fields
|
||||
field :id, as: :id
|
||||
field :build_configuration, as: :belongs_to
|
||||
field :namespace, as: :text
|
||||
field :name, as: :text
|
||||
field :version, as: :text
|
||||
field :details, as: :code
|
||||
end
|
||||
end
|
||||
4
app/controllers/avo/build_packs_controller.rb
Normal file
@@ -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::BuildPacksController < Avo::ResourcesController
|
||||
end
|
||||
22
app/controllers/build_packs_controller.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
class BuildPacksController < ApplicationController
|
||||
def search
|
||||
result = Buildpacks::Search.execute(query: params[:q])
|
||||
if result.success?
|
||||
render json: result.results
|
||||
else
|
||||
render json: { error: "Failed to search buildpacks" }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def details
|
||||
result = Buildpacks::Details.execute(
|
||||
namespace: params[:namespace],
|
||||
name: params[:name]
|
||||
)
|
||||
if result.success?
|
||||
render json: result.result
|
||||
else
|
||||
render json: { error: "Failed to fetch buildpack details" }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
257
app/javascript/controllers/buildpack_fields_controller.js
Normal file
@@ -0,0 +1,257 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import Sortable from "sortablejs"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["list", "template", "modal", "baseBuilder", "availableBuildpacks", "selectedBuildpacks"]
|
||||
static values = {
|
||||
packs: Object
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.selectedPacks = []
|
||||
this.initializeSortable()
|
||||
|
||||
// Listen for buildpack selection from search
|
||||
this.element.addEventListener("buildpack-search:buildpack-selected", this.handleSearchSelection.bind(this))
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.element.removeEventListener("buildpack-search:buildpack-selected", this.handleSearchSelection.bind(this))
|
||||
}
|
||||
|
||||
handleSearchSelection(event) {
|
||||
const { namespace, name, version, description } = event.detail
|
||||
|
||||
// Create a pack object with buildpack.webp as the default image
|
||||
const pack = {
|
||||
key: `${namespace}/${name}`,
|
||||
namespace: namespace,
|
||||
name: name,
|
||||
version: version || '',
|
||||
image: '/images/languages/buildpack.webp',
|
||||
description: description || '',
|
||||
reference_type: 'registry'
|
||||
}
|
||||
|
||||
// Check if already selected
|
||||
if (!this.selectedPacks.some(p => p.key === pack.key)) {
|
||||
this.selectedPacks.push(pack)
|
||||
this.displayAvailableBuildpacks()
|
||||
this.renderSelectedBuildpacks()
|
||||
}
|
||||
}
|
||||
|
||||
initializeSortable() {
|
||||
if (this.hasListTarget) {
|
||||
this.sortable = Sortable.create(this.listTarget, {
|
||||
animation: 150,
|
||||
handle: ".drag-handle",
|
||||
ghostClass: "opacity-50"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
initializeModalSortable() {
|
||||
if (this.hasSelectedBuildpacksTarget && !this.modalSortable) {
|
||||
this.modalSortable = Sortable.create(this.selectedBuildpacksTarget, {
|
||||
animation: 150,
|
||||
ghostClass: "opacity-50",
|
||||
onEnd: () => {
|
||||
this.updateSelectedPacksOrder()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedPacksOrder() {
|
||||
// Get the current DOM order and update selectedPacks array
|
||||
const elements = this.selectedBuildpacksTarget.querySelectorAll('[data-key]')
|
||||
this.selectedPacks = Array.from(elements).map(el => {
|
||||
const key = el.dataset.key
|
||||
return { key, ...this.packsValue[key] }
|
||||
})
|
||||
}
|
||||
|
||||
openModal() {
|
||||
// Repopulate selectedPacks from existing buildpacks in the form
|
||||
this.selectedPacks = this.getExistingBuildpacks()
|
||||
this.displayAvailableBuildpacks()
|
||||
this.renderSelectedBuildpacks()
|
||||
this.modalTarget.showModal()
|
||||
// Initialize sortable after modal is shown
|
||||
setTimeout(() => this.initializeModalSortable(), 100)
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.modalTarget.close()
|
||||
}
|
||||
|
||||
getExistingBuildpacks() {
|
||||
const existingPacks = []
|
||||
const cards = this.listTarget.querySelectorAll('.card')
|
||||
|
||||
cards.forEach(card => {
|
||||
const namespaceInput = card.querySelector('input[name*="[namespace]"]')
|
||||
const nameInput = card.querySelector('input[name*="[name]"]')
|
||||
|
||||
if (namespaceInput && nameInput) {
|
||||
const key = `${namespaceInput.value}/${nameInput.value}`
|
||||
const pack = this.packsValue[key]
|
||||
if (pack) {
|
||||
existingPacks.push({ key, ...pack })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return existingPacks
|
||||
}
|
||||
|
||||
displayAvailableBuildpacks() {
|
||||
const builder = this.baseBuilderTarget.value
|
||||
const namespace = this.detectNamespace(builder)
|
||||
|
||||
if (!namespace) {
|
||||
this.availableBuildpacksTarget.innerHTML = '<div class="text-sm text-gray-500 p-4 text-center">Please select a base builder first</div>'
|
||||
return
|
||||
}
|
||||
|
||||
const availablePacks = Object.entries(this.packsValue)
|
||||
.filter(([key, pack]) => pack.namespace === namespace)
|
||||
.map(([key, pack]) => ({ key, ...pack }))
|
||||
|
||||
if (availablePacks.length === 0) {
|
||||
this.availableBuildpacksTarget.innerHTML = '<div class="text-sm text-gray-500 p-4 text-center">No buildpacks available for this builder</div>'
|
||||
return
|
||||
}
|
||||
|
||||
this.renderAvailableBuildpacks(availablePacks)
|
||||
}
|
||||
|
||||
renderAvailableBuildpacks(packs) {
|
||||
const filteredPacks = packs.filter(pack =>
|
||||
!this.selectedPacks.some(selected => selected.key === pack.key)
|
||||
)
|
||||
|
||||
if (filteredPacks.length === 0) {
|
||||
this.availableBuildpacksTarget.innerHTML = '<div class="text-sm text-gray-500 p-4 text-center">All buildpacks selected</div>'
|
||||
return
|
||||
}
|
||||
|
||||
const html = filteredPacks.map(pack => `
|
||||
<div class="p-3 hover:bg-base-200 cursor-pointer border-b border-base-300 flex items-center gap-3"
|
||||
data-action="click->buildpack-fields#selectBuildpack"
|
||||
data-key="${pack.key}">
|
||||
<img src="${pack.image}" alt="${pack.key}" class="w-10 h-10 object-contain" />
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">${pack.namespace}/${pack.name}</div>
|
||||
<div class="text-sm text-gray-600">${pack.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
|
||||
this.availableBuildpacksTarget.innerHTML = html
|
||||
}
|
||||
|
||||
renderSelectedBuildpacks() {
|
||||
if (this.selectedPacks.length === 0) {
|
||||
this.selectedBuildpacksTarget.innerHTML = '<div class="text-sm text-gray-500 p-4 text-center">No buildpacks selected</div>'
|
||||
if (this.modalSortable) {
|
||||
this.modalSortable.destroy()
|
||||
this.modalSortable = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const html = this.selectedPacks.map((pack, index) => `
|
||||
<div class="p-3 hover:bg-base-200 cursor-move border-b border-base-300 flex items-center gap-3"
|
||||
data-key="${pack.key}"
|
||||
data-index="${index}">
|
||||
<img src="${pack.image}" alt="${pack.key}" class="w-10 h-10 object-contain" />
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">${pack.namespace}/${pack.name}</div>
|
||||
<div class="text-sm text-gray-600">${pack.description}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-xs btn-circle btn-ghost" data-action="click->buildpack-fields#deselectBuildpack" data-index="${index}">
|
||||
<iconify-icon icon="mdi:close" width="16" height="16"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')
|
||||
|
||||
this.selectedBuildpacksTarget.innerHTML = html
|
||||
|
||||
// Reinitialize sortable after rendering
|
||||
if (this.modalSortable) {
|
||||
this.modalSortable.destroy()
|
||||
this.modalSortable = null
|
||||
}
|
||||
this.initializeModalSortable()
|
||||
}
|
||||
|
||||
detectNamespace(builder) {
|
||||
if (!builder) return null
|
||||
|
||||
if (builder.includes("paketo")) {
|
||||
return "paketo-buildpacks"
|
||||
} else if (builder.includes("heroku")) {
|
||||
return "heroku"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
selectBuildpack(event) {
|
||||
const key = event.currentTarget.dataset.key
|
||||
const pack = { key, ...this.packsValue[key] }
|
||||
|
||||
this.selectedPacks.push(pack)
|
||||
this.displayAvailableBuildpacks()
|
||||
this.renderSelectedBuildpacks()
|
||||
}
|
||||
|
||||
deselectBuildpack(event) {
|
||||
event.stopPropagation()
|
||||
const index = parseInt(event.currentTarget.dataset.index)
|
||||
this.selectedPacks.splice(index, 1)
|
||||
this.displayAvailableBuildpacks()
|
||||
this.renderSelectedBuildpacks()
|
||||
}
|
||||
|
||||
addSelectedBuildpacks() {
|
||||
// Clear existing buildpacks from the list
|
||||
this.listTarget.innerHTML = ''
|
||||
|
||||
this.selectedPacks.forEach((pack, index) => {
|
||||
const template = this.templateTarget.content || this.templateTarget
|
||||
const clone = template.cloneNode(true)
|
||||
|
||||
const img = clone.querySelector('[data-template-image]')
|
||||
img.src = pack.image
|
||||
img.alt = pack.key
|
||||
|
||||
const title = clone.querySelector('[data-template-title]')
|
||||
title.textContent = `${pack.namespace}/${pack.name}`
|
||||
|
||||
const description = clone.querySelector('[data-template-description]')
|
||||
description.textContent = pack.description
|
||||
|
||||
const namespaceInput = clone.querySelector('[data-template-namespace]')
|
||||
namespaceInput.value = pack.namespace
|
||||
|
||||
const nameInput = clone.querySelector('[data-template-name]')
|
||||
nameInput.value = pack.name
|
||||
|
||||
const referenceTypeInput = clone.querySelector('[data-template-reference-type]')
|
||||
referenceTypeInput.value = pack.reference_type
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.appendChild(clone)
|
||||
|
||||
this.listTarget.insertAdjacentHTML("beforeend", container.innerHTML)
|
||||
})
|
||||
|
||||
this.closeModal()
|
||||
}
|
||||
|
||||
remove(event) {
|
||||
event.target.closest(".card").remove()
|
||||
}
|
||||
}
|
||||
46
app/javascript/controllers/buildpack_search_controller.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import AsyncSearchDropdownController from "./components/async_search_dropdown_controller"
|
||||
|
||||
export default class extends AsyncSearchDropdownController {
|
||||
async fetchResults(query) {
|
||||
const response = await fetch(`/build_packs/search?q=${encodeURIComponent(query)}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch buildpacks')
|
||||
}
|
||||
const data = await response.json()
|
||||
return data
|
||||
}
|
||||
|
||||
renderItem(buildpack) {
|
||||
const latest = buildpack.latest
|
||||
const verifiedBadge = latest.verified
|
||||
? '<span class="badge badge-primary badge-sm">Verified</span>'
|
||||
: ''
|
||||
|
||||
return `
|
||||
<div class="flex justify-between items-start text-left">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="font-semibold">
|
||||
${latest.namespace}/${latest.name}
|
||||
</div>
|
||||
<div class="text-sm text-base-content/60">${latest.description || 'No description'}</div>
|
||||
<div class="text-xs text-base-content/70 mt-1">
|
||||
Latest: ${latest.version} | ${latest.licenses?.join(', ') || 'No license info'}
|
||||
</div>
|
||||
</div>
|
||||
${verifiedBadge}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
onItemSelect(buildpack, itemElement) {
|
||||
const latest = buildpack.latest
|
||||
this.dispatch("buildpack-selected", {
|
||||
detail: {
|
||||
namespace: latest.namespace,
|
||||
name: latest.name,
|
||||
version: latest.version,
|
||||
description: latest.description
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { debounce } from "../../utils"
|
||||
|
||||
/**
|
||||
* Base controller for async search dropdowns with autocomplete
|
||||
*
|
||||
* Child controllers must implement:
|
||||
* - fetchResults(query): Promise<Array> - Fetch and return search results
|
||||
* - renderItem(item): String - Return HTML string for a single item
|
||||
* - onItemSelect(item, itemElement): void - Handle item selection
|
||||
*
|
||||
* Optional overrides:
|
||||
* - getInputElement(): HTMLElement - Get the input element (default: finds input in this.element)
|
||||
* - shouldSearch(query): Boolean - Determine if search should be performed (default: non-empty query)
|
||||
* - getDebounceDelay(): Number - Debounce delay in ms (default: 500)
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.input = this.getInputElement()
|
||||
|
||||
if (!this.input) {
|
||||
console.error('AsyncSearchDropdown: No input element found')
|
||||
return
|
||||
}
|
||||
|
||||
// Disable browser autocomplete
|
||||
this.input.setAttribute('autocomplete', 'off')
|
||||
|
||||
// Create dropdown
|
||||
this.dropdown = this.createDropdown()
|
||||
this.element.appendChild(this.dropdown)
|
||||
|
||||
// Bind search handler with debounce
|
||||
this.searchHandler = debounce(this.performSearch.bind(this), this.getDebounceDelay())
|
||||
this.input.addEventListener('input', this.searchHandler)
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
this.clickOutsideHandler = this.handleClickOutside.bind(this)
|
||||
document.addEventListener('click', this.clickOutsideHandler)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.input) {
|
||||
this.input.removeEventListener('input', this.searchHandler)
|
||||
}
|
||||
document.removeEventListener('click', this.clickOutsideHandler)
|
||||
}
|
||||
|
||||
createDropdown() {
|
||||
const dropdown = document.createElement('ul')
|
||||
dropdown.className = 'hidden absolute z-10 w-full mt-1 menu bg-base-200 block rounded-box shadow-lg max-h-[300px] overflow-y-auto'
|
||||
return dropdown
|
||||
}
|
||||
|
||||
getInputElement() {
|
||||
return this.element.querySelector('input')
|
||||
}
|
||||
|
||||
getDebounceDelay() {
|
||||
return 500
|
||||
}
|
||||
|
||||
shouldSearch(query) {
|
||||
return query.trim().length > 0
|
||||
}
|
||||
|
||||
async performSearch() {
|
||||
const query = this.input.value
|
||||
|
||||
if (!this.shouldSearch(query)) {
|
||||
this.hideDropdown()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.showLoading()
|
||||
const results = await this.fetchResults(query)
|
||||
this.renderResults(results)
|
||||
} catch (error) {
|
||||
console.error('Search error:', error)
|
||||
this.showError(error.message || 'Failed to fetch results')
|
||||
}
|
||||
}
|
||||
|
||||
renderResults(results) {
|
||||
if (!results || results.length === 0) {
|
||||
this.showEmpty()
|
||||
return
|
||||
}
|
||||
|
||||
this.dropdown.innerHTML = results.map((item, index) => `
|
||||
<li class="cursor-pointer hover:bg-base-300 p-2" data-index="${index}">
|
||||
${this.renderItem(item)}
|
||||
</li>
|
||||
`).join('')
|
||||
|
||||
// Store results for later access
|
||||
this.currentResults = results
|
||||
|
||||
// Add click handlers
|
||||
this.dropdown.querySelectorAll('li').forEach((li, index) => {
|
||||
li.addEventListener('click', () => {
|
||||
this.selectItem(results[index], li)
|
||||
})
|
||||
})
|
||||
|
||||
this.showDropdown()
|
||||
}
|
||||
|
||||
selectItem(item, itemElement) {
|
||||
this.onItemSelect(item, itemElement)
|
||||
this.clearInput()
|
||||
this.hideDropdown()
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
if (this.input) {
|
||||
this.input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
showDropdown() {
|
||||
this.dropdown.classList.remove('hidden')
|
||||
}
|
||||
|
||||
hideDropdown() {
|
||||
this.dropdown.classList.add('hidden')
|
||||
this.dropdown.innerHTML = ''
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
this.dropdown.innerHTML = `
|
||||
<li class="p-4 text-center flex items-center justify-center gap-2">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<span>Searching...</span>
|
||||
</li>
|
||||
`
|
||||
this.showDropdown()
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.dropdown.innerHTML = `
|
||||
<li class="p-4 text-center text-error">
|
||||
${message}
|
||||
</li>
|
||||
`
|
||||
this.showDropdown()
|
||||
}
|
||||
|
||||
showEmpty() {
|
||||
this.dropdown.innerHTML = `
|
||||
<li class="p-4 text-center text-base-content/60">
|
||||
No results found
|
||||
</li>
|
||||
`
|
||||
this.showDropdown()
|
||||
}
|
||||
|
||||
handleClickOutside(event) {
|
||||
if (!this.element.contains(event.target)) {
|
||||
this.hideDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
// Methods to be implemented by child controllers
|
||||
async fetchResults(query) {
|
||||
throw new Error('fetchResults must be implemented by child controller')
|
||||
}
|
||||
|
||||
renderItem(item) {
|
||||
throw new Error('renderItem must be implemented by child controller')
|
||||
}
|
||||
|
||||
onItemSelect(item, itemElement) {
|
||||
throw new Error('onItemSelect must be implemented by child controller')
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,33 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import AsyncSearchDropdownController from "./components/async_search_dropdown_controller"
|
||||
import { renderHelmChartCard, helmChartHeader } from "../utils/helm_charts"
|
||||
import { debounce } from "../utils"
|
||||
|
||||
export default class extends Controller {
|
||||
export default class extends AsyncSearchDropdownController {
|
||||
static values = {
|
||||
chartName: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.input = this.element.querySelector(`input[name="add_on[metadata][helm_chart][helm_chart.name]"]`)
|
||||
// disable autocomplete
|
||||
this.input.setAttribute('autocomplete', 'off')
|
||||
|
||||
// Create and append dropdown
|
||||
this.dropdown = document.createElement('ul')
|
||||
this.dropdown.className = 'hidden absolute z-10 w-full mt-1 menu bg-base-200 block rounded-box shadow-lg max-h-[300px] overflow-y-auto'
|
||||
this.element.appendChild(this.dropdown)
|
||||
|
||||
// Bind search handler with debounce
|
||||
this.input.addEventListener('input', debounce(this.performSearch.bind(this), 500));
|
||||
getInputElement() {
|
||||
return this.element.querySelector(`input[name="add_on[metadata][helm_chart][helm_chart.name]"]`)
|
||||
}
|
||||
|
||||
async performSearch() {
|
||||
if (!this.input.value.trim()) {
|
||||
this.hideDropdown()
|
||||
return
|
||||
async fetchResults(query) {
|
||||
const response = await fetch(`/add_ons/search?q=${encodeURIComponent(query)}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch helm charts')
|
||||
}
|
||||
|
||||
const url = `/add_ons/search?q=${this.input.value}`
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
|
||||
this.renderResults(data.packages)
|
||||
return data.packages
|
||||
}
|
||||
|
||||
renderResults(packages) {
|
||||
if (!packages.length) {
|
||||
this.hideDropdown()
|
||||
return
|
||||
}
|
||||
|
||||
this.dropdown.innerHTML = packages.map(pkg => `
|
||||
<li class="p-2" data-package-name="${pkg.name}" data-package-data="${encodeURIComponent(JSON.stringify(pkg))}">
|
||||
${helmChartHeader(pkg)}
|
||||
</li>
|
||||
`).join('')
|
||||
|
||||
// Add click handlers to all list items
|
||||
this.dropdown.querySelectorAll('li').forEach(li => {
|
||||
li.addEventListener('click', () => {
|
||||
this.input.parentElement.classList.add('hidden')
|
||||
this.input.value = li.dataset.packageName
|
||||
const packageData = JSON.parse(decodeURIComponent(li.dataset.packageData));
|
||||
this.hideDropdown()
|
||||
const chartUrl = `${packageData.repository.name}/${packageData.name}`
|
||||
document.querySelector(`input[name="add_on[chart_url]"]`).value = chartUrl
|
||||
this.element.appendChild(renderHelmChartCard(packageData))
|
||||
})
|
||||
})
|
||||
|
||||
this.showDropdown()
|
||||
renderItem(pkg) {
|
||||
return helmChartHeader(pkg)
|
||||
}
|
||||
|
||||
showDropdown() {
|
||||
this.dropdown.classList.remove('hidden')
|
||||
}
|
||||
|
||||
hideDropdown() {
|
||||
this.dropdown.classList.add('hidden')
|
||||
onItemSelect(pkg, itemElement) {
|
||||
this.input.parentElement.classList.add('hidden')
|
||||
this.input.value = pkg.name
|
||||
const chartUrl = `${pkg.repository.name}/${pkg.name}`
|
||||
document.querySelector(`input[name="add_on[chart_url]"]`).value = chartUrl
|
||||
this.element.appendChild(renderHelmChartCard(pkg))
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ class BuildConfiguration < ApplicationRecord
|
||||
belongs_to :project
|
||||
belongs_to :build_cloud, optional: true
|
||||
belongs_to :provider
|
||||
has_many :build_packs, -> { order(:build_order) }, dependent: :destroy
|
||||
|
||||
validates_presence_of :project, :provider, :driver
|
||||
validates_presence_of :image_repository
|
||||
@@ -48,9 +49,19 @@ class BuildConfiguration < ApplicationRecord
|
||||
}
|
||||
|
||||
def self.permit_params(params)
|
||||
params.permit(:image_repository, :driver, :build_cloud_id, :provider_id)
|
||||
params.permit(:image_repository, :driver, :build_cloud_id, :provider_id, :context_directory, :dockerfile_path, :build_type, :buildpack_base_builder)
|
||||
end
|
||||
|
||||
def self.available_buildpacks
|
||||
packs_file = Rails.root.join("resources", "build_packs", "packs.yaml")
|
||||
YAML.load_file(packs_file)
|
||||
end
|
||||
|
||||
enum :build_type, {
|
||||
dockerfile: 0,
|
||||
buildpacks: 1
|
||||
}
|
||||
|
||||
enum :driver, {
|
||||
cloud: 0,
|
||||
docker: 1,
|
||||
|
||||
82
app/models/build_pack.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: build_packs
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# build_order :integer not null
|
||||
# details :jsonb
|
||||
# name :string
|
||||
# namespace :string
|
||||
# reference_type :integer not null
|
||||
# uri :text
|
||||
# version :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# build_configuration_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_build_packs_on_build_configuration_id (build_configuration_id)
|
||||
# index_build_packs_on_config_type_namespace_name (build_configuration_id,reference_type,namespace,name)
|
||||
# index_build_packs_on_config_uri (build_configuration_id,uri)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (build_configuration_id => build_configurations.id)
|
||||
#
|
||||
class BuildPack < ApplicationRecord
|
||||
VERIFIED_NAMESPACES = %w[io.buildpacks paketo-buildpacks heroku tanzu-buildpacks].freeze
|
||||
|
||||
belongs_to :build_configuration
|
||||
validates_presence_of :build_order
|
||||
|
||||
enum :reference_type, {
|
||||
registry: 0,
|
||||
git: 1,
|
||||
url: 2
|
||||
}
|
||||
|
||||
validates :reference_type, presence: true
|
||||
validates :namespace, presence: true, if: :registry?
|
||||
validates :name, presence: true, if: :registry?
|
||||
validates :uri, presence: true, unless: :registry?
|
||||
|
||||
# Helper method to get full buildpack reference for pack CLI
|
||||
def reference
|
||||
case reference_type.to_sym
|
||||
when :registry
|
||||
if version.present?
|
||||
"#{namespace}/#{name}:#{version}"
|
||||
else
|
||||
"#{namespace}/#{name}"
|
||||
end
|
||||
else
|
||||
uri
|
||||
end
|
||||
end
|
||||
|
||||
# Synthetic property to check if buildpack is from a verified namespace
|
||||
def verified?
|
||||
registry? && VERIFIED_NAMESPACES.include?(namespace)
|
||||
end
|
||||
|
||||
# Display name for UI
|
||||
def display_name
|
||||
if registry?
|
||||
"#{namespace}/#{name}"
|
||||
elsif git? && uri.present?
|
||||
# Extract repo name from git URL
|
||||
uri.split('/').last.to_s.gsub('.git', '')
|
||||
else
|
||||
uri
|
||||
end
|
||||
end
|
||||
|
||||
def key
|
||||
"#{namespace}/#{name}"
|
||||
end
|
||||
|
||||
def static_info
|
||||
BuildConfiguration.available_buildpacks[key] || {}
|
||||
end
|
||||
end
|
||||
@@ -42,7 +42,7 @@ module Builders
|
||||
command += [ "--push" ] # Push directly to registry
|
||||
command += [ "--progress", "plain" ]
|
||||
command += [ "-t", project.container_image_reference ]
|
||||
command += [ "-f", File.join(repository_path, project.dockerfile_path) ]
|
||||
command += [ "-f", File.join(repository_path, project.build_configuration.dockerfile_path) ]
|
||||
|
||||
# Add build arguments
|
||||
project.environment_variables.each do |envar|
|
||||
@@ -56,7 +56,7 @@ module Builders
|
||||
command += [ "--push" ]
|
||||
|
||||
# Add build context
|
||||
command << File.join(repository_path, project.docker_build_context_directory)
|
||||
command << File.join(repository_path, project.build_configuration.context_directory)
|
||||
|
||||
command
|
||||
end
|
||||
|
||||
@@ -6,46 +6,18 @@ module Builders
|
||||
class Docker < Builders::Base
|
||||
# Build and push the Docker image
|
||||
def build_image(repository_path)
|
||||
execute_docker_build(repository_path)
|
||||
if project.build_configuration.buildpacks?
|
||||
Builders::Frontends::BuildpackBuilder.new(build).build_with_buildpacks(repository_path)
|
||||
else
|
||||
Builders::Frontends::DockerfileBuilder.new(build).build_with_dockerfile(repository_path)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def execute_docker_build(repository_path)
|
||||
docker_build_command = construct_buildx_command(repository_path)
|
||||
|
||||
# Create a new instance of RunAndLog with the build object as the loggable and killable
|
||||
runner = Cli::RunAndLog.new(build, killable: build)
|
||||
|
||||
# Call the runner with the command (joined as a string since RunAndLog expects a string)
|
||||
runner.call(docker_build_command.join(" "))
|
||||
rescue Cli::CommandFailedError => e
|
||||
raise "Docker build failed: #{e.message}"
|
||||
end
|
||||
|
||||
def construct_buildx_command(repository_path)
|
||||
docker_build_command = [
|
||||
"docker",
|
||||
"--context=default",
|
||||
"buildx",
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--platform", "linux/amd64",
|
||||
"-t", project.container_image_reference,
|
||||
"-f", File.join(repository_path, project.dockerfile_path)
|
||||
]
|
||||
|
||||
# Add environment variables to the build command
|
||||
project.environment_variables.each do |envar|
|
||||
docker_build_command.push("--build-arg", "#{envar.name}=\"#{envar.value}\"")
|
||||
end
|
||||
|
||||
docker_build_command.push("--push")
|
||||
|
||||
# Add the build context directory at the end
|
||||
docker_build_command.push(File.join(repository_path, project.docker_build_context_directory))
|
||||
Rails.logger.info("Docker build command: `#{docker_build_command.join(" ")}`")
|
||||
docker_build_command
|
||||
# Pack publishes during build with --publish flag
|
||||
def publish_during_build?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
86
app/services/builders/frontends/buildpack_builder.rb
Normal file
@@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Builders::Frontends::BuildpackBuilder
|
||||
attr_accessor :build, :project
|
||||
|
||||
def initialize(build)
|
||||
@build = build
|
||||
@project = build.project
|
||||
end
|
||||
|
||||
# Build image using Cloud Native Buildpacks
|
||||
def build_with_buildpacks(repository_path)
|
||||
build_config = build.project.build_configuration
|
||||
|
||||
build.info("Building with Cloud Native Buildpacks", color: :blue)
|
||||
build.info("Builder: #{build_config.buildpack_base_builder}", color: :cyan)
|
||||
build.info("Context: #{build_config.context_directory}", color: :cyan)
|
||||
|
||||
# Log buildpacks in order
|
||||
if build_config.build_packs.any?
|
||||
build.info("Buildpacks:", color: :cyan)
|
||||
build_config.build_packs.each do |pack|
|
||||
verified_badge = pack.verified? ? " [verified]" : ""
|
||||
build.info(" #{pack.build_order + 1}. #{pack.reference}#{verified_badge}")
|
||||
end
|
||||
else
|
||||
build.info("No buildpacks specified - builder will auto-detect", color: :yellow)
|
||||
end
|
||||
|
||||
# Generate and execute pack command
|
||||
command = generate_pack_command(repository_path, build_config)
|
||||
|
||||
build.info("Running pack build...", color: :green)
|
||||
run_pack_command(command)
|
||||
|
||||
# Push image if not published during build
|
||||
push_image_after_build unless publish_during_build?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_pack_command(repository_path, build_config)
|
||||
image_name = build_config.container_image_reference
|
||||
context_path = File.join(repository_path, build_config.context_directory)
|
||||
|
||||
command = [
|
||||
"pack", "build", image_name,
|
||||
"--builder", build_config.buildpack_base_builder,
|
||||
"--path", context_path
|
||||
]
|
||||
|
||||
# Add buildpacks in order
|
||||
build_config.build_packs.each do |pack|
|
||||
command += [ "--buildpack", pack.reference ]
|
||||
end
|
||||
|
||||
# Add publish flag if supported by driver
|
||||
command << "--publish" if publish_during_build?
|
||||
|
||||
# Add pull policy to always pull latest builder
|
||||
command += [ "--pull-policy", "always" ]
|
||||
|
||||
# Trust builder (required for some builders)
|
||||
command << "--trust-builder"
|
||||
|
||||
command.shelljoin
|
||||
end
|
||||
|
||||
def run_pack_command(command)
|
||||
runner = Cli::RunAndLog.new(build, killable: build)
|
||||
runner.call(command)
|
||||
rescue Cli::CommandFailedError => e
|
||||
raise Projects::BuildJob::BuildFailure, "Pack build failed: #{e.message}"
|
||||
end
|
||||
|
||||
# Override in including class if push happens during build
|
||||
def publish_during_build?
|
||||
false
|
||||
end
|
||||
|
||||
# Override in including class to implement image push logic
|
||||
def push_image_after_build
|
||||
# Default: assume publish_during_build? is true
|
||||
# Concrete builders should override if they need separate push
|
||||
end
|
||||
end
|
||||
45
app/services/builders/frontends/dockerfile_builder.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
class Builders::Frontends::DockerfileBuilder
|
||||
attr_accessor :build, :project
|
||||
|
||||
def initialize(build)
|
||||
@build = build
|
||||
@project = build.project
|
||||
end
|
||||
|
||||
def build_with_dockerfile(repository_path)
|
||||
docker_build_command = construct_buildx_command(repository_path)
|
||||
|
||||
# Create a new instance of RunAndLog with the build object as the loggable and killable
|
||||
runner = Cli::RunAndLog.new(build, killable: build)
|
||||
|
||||
# Call the runner with the command (joined as a string since RunAndLog expects a string)
|
||||
runner.call(docker_build_command.join(" "))
|
||||
rescue Cli::CommandFailedError => e
|
||||
raise "Docker build failed: #{e.message}"
|
||||
end
|
||||
|
||||
def construct_buildx_command(repository_path)
|
||||
docker_build_command = [
|
||||
"docker",
|
||||
"--context=default",
|
||||
"buildx",
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--platform", "linux/amd64",
|
||||
"-t", project.container_image_reference,
|
||||
"-f", File.join(repository_path, project.build_configuration.dockerfile_path)
|
||||
]
|
||||
|
||||
# Add environment variables to the build command
|
||||
project.environment_variables.each do |envar|
|
||||
docker_build_command.push("--build-arg", "#{envar.name}=\"#{envar.value}\"")
|
||||
end
|
||||
|
||||
docker_build_command.push("--push")
|
||||
|
||||
# Add the build context directory at the end
|
||||
docker_build_command.push(File.join(repository_path, project.build_configuration.context_directory))
|
||||
Rails.logger.info("Docker build command: `#{docker_build_command.join(" ")}`")
|
||||
docker_build_command
|
||||
end
|
||||
end
|
||||
@@ -64,6 +64,8 @@ class Git::Github::Client < Git::Client
|
||||
|
||||
def webhook_exists?
|
||||
webhook.present?
|
||||
rescue Octokit::NotFound
|
||||
false
|
||||
end
|
||||
|
||||
def remove_webhook!
|
||||
|
||||
7
app/views/build_packs/_search.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="relative" data-controller="buildpack-search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for buildpacks (e.g., ruby, node, python)..."
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<div class="mt-6 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-semibold mb-2">Public Git Repository</h3>
|
||||
<p class="mb-4">Use a public repository by entering the URL below. Features like PR Previews and Auto-Deploy are not available if the repository has not been configured for Render.</p>
|
||||
<p class="mb-4">Use a public repository by entering the URL below. Features like PR Previews and Auto-Deploy are not available if the repository has not been configured for Canine.</p>
|
||||
<div class="flex gap-4">
|
||||
<input type="text" placeholder="czhu12/whiteboarder" data-github-select-repository-target="publicRepository" class="input input-bordered w-full mb-4" />
|
||||
<button type="button" data-action="github-select-repository#selectPublicRepository" class="btn btn-outline">Continue</button>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<ul>
|
||||
<% current_account.projects.order(created_at: :desc).each do |project| %>
|
||||
<li>
|
||||
<%= link_to root_projects_path(project), class: "hover:bg-base-content/15 #{'active' if request.path.start_with?("/projects/#{project.id}")}" do %>
|
||||
<%= link_to root_projects_path(project), class: "hover:bg-base-content/15 #{'active' if request.path.start_with?(project_path(project))}" do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= project.name %>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@
|
||||
<ul>
|
||||
<% current_account.clusters.order(created_at: :desc).each do |cluster| %>
|
||||
<li>
|
||||
<%= link_to cluster_path(cluster), class: "hover:bg-base-content/15 #{'active' if current_page?(cluster_path(cluster))}" do %>
|
||||
<%= link_to cluster_path(cluster), class: "hover:bg-base-content/15 #{'active' if request.path.start_with?(cluster_path(cluster))}" do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= cluster.name %>
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@
|
||||
<ul>
|
||||
<% current_account.add_ons.order(created_at: :desc).each do |add_on| %>
|
||||
<li>
|
||||
<%= link_to add_on_path(add_on), class: "hover:bg-base-content/15 #{'active' if current_page?(add_on_path(add_on))}" do %>
|
||||
<%= link_to add_on_path(add_on), class: "hover:bg-base-content/15 #{'active' if request.path.start_with?(add_on_path(add_on))}" do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= add_on.name %>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
|
||||
<div class="flex flex-row gap-6">
|
||||
<div class="flex-shrink my-4 border-l-2 border-base-300 pl-4">
|
||||
</div>
|
||||
<div data-controller="buildpack-fields" data-buildpack-fields-packs-value="<%= BuildConfiguration.available_buildpacks.to_json %>">
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-md">
|
||||
<label class="label">
|
||||
<span class="label-text">Base Builder</span>
|
||||
</label>
|
||||
<%= bc_form.select(
|
||||
:buildpack_base_builder,
|
||||
options_for_select(
|
||||
[
|
||||
["heroku/builder-classic:22", "heroku/builder-classic:22"],
|
||||
["heroku/builder:22", "heroku/builder:22"],
|
||||
["heroku/builder:24", "heroku/builder:24"],
|
||||
["heroku/buildpacks:18", "heroku/buildpacks:18"],
|
||||
["heroku/buildpacks:20", "heroku/buildpacks:20"],
|
||||
["paketobuildpacks/builder-jammy-full:latest", "paketobuildpacks/builder-jammy-full:latest"],
|
||||
["paketobuildpacks/builder:full", "paketobuildpacks/builder:full"]
|
||||
],
|
||||
selected: build_configuration.buildpack_base_builder || "heroku/buildpacks:20"
|
||||
),
|
||||
{ include_blank: "Select a base builder..." },
|
||||
{ class: "select select-bordered w-full", data: { buildpack_fields_target: "baseBuilder" } }
|
||||
) %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Select the base builder image for Cloud Native Buildpacks</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-md">
|
||||
<label class="label">
|
||||
<span class="label-text">Buildpacks</span>
|
||||
</label>
|
||||
<div data-buildpack-fields-target="list" class="space-y-2">
|
||||
<% build_configuration.build_packs.each do |build_pack| %>
|
||||
<%= render "projects/build_configurations/buildpack_item", build_pack: build_pack %>
|
||||
<% end %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline btn-primary mt-2" data-action="click->buildpack-fields#openModal">
|
||||
<iconify-icon icon="mdi:plus" width="20" height="20"></iconify-icon>
|
||||
Add Buildpack
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template data-buildpack-fields-target="template">
|
||||
<%= render "projects/build_configurations/buildpack_item" %>
|
||||
</template>
|
||||
|
||||
<dialog data-buildpack-fields-target="modal" class="modal">
|
||||
<div class="modal-box w-11/12 max-w-5xl">
|
||||
<h3 class="font-bold text-lg">Add Buildpacks</h3>
|
||||
<p class="py-2 text-sm text-gray-600">Select buildpacks to add to your build configuration.</p>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<div>
|
||||
<h4 class="font-semibold mb-2">Selected Buildpacks</h4>
|
||||
<div data-buildpack-fields-target="selectedBuildpacks" class="border border-base-300 rounded-lg max-h-48 overflow-y-auto">
|
||||
<div class="text-sm text-gray-500 p-4 text-center">No buildpacks selected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-semibold mb-2">Official Buildpacks</h4>
|
||||
<div data-buildpack-fields-target="availableBuildpacks" class="border border-base-300 rounded-lg max-h-64 overflow-y-auto">
|
||||
<!-- Official buildpacks will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-semibold mb-2">Search Registry</h4>
|
||||
<%= render "build_packs/search" %>
|
||||
<div data-buildpack-fields-target="searchResults" class="border border-base-300 rounded-lg max-h-64 overflow-y-auto mt-2">
|
||||
<!-- Search results will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn btn-ghost" data-action="click->buildpack-fields#closeModal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" data-action="click->buildpack-fields#addSelectedBuildpacks">Add Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<% build_pack = local_assigns[:build_pack] || BuildPack.new %>
|
||||
<div class="card bg-base-200 p-4">
|
||||
<div class="flex gap-3 items-center">
|
||||
<div class="drag-handle cursor-move flex items-center">
|
||||
<iconify-icon icon="mdi:drag-vertical" width="20" height="20" class="text-gray-500"></iconify-icon>
|
||||
</div>
|
||||
<img data-template-image class="w-10 h-10 object-contain" src="<%= build_pack.static_info['image'] || '/images/languages/buildpack.webp' %>" />
|
||||
<div class="flex-1">
|
||||
<div class="font-medium" data-template-title><%= "#{build_pack.namespace}/#{build_pack.name}" %></div>
|
||||
<div class="text-sm text-gray-600" data-template-description><%= build_pack.details.dig('latest', 'description') %></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-ghost" data-action="click->buildpack-fields#remove">
|
||||
<iconify-icon icon="mdi:close" width="20" height="20"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" data-template-namespace name="project[build_configuration][build_packs_attributes][][namespace]" value="<%= build_pack.namespace %>" />
|
||||
<input type="hidden" data-template-name name="project[build_configuration][build_packs_attributes][][name]" value="<%= build_pack.name %>" />
|
||||
<input type="hidden" data-template-reference-type name="project[build_configuration][build_packs_attributes][][reference_type]" value="<%= build_pack.reference_type %>" />
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="flex flex-row gap-6">
|
||||
<div class="flex-shrink my-4 border-l-2 border-base-300 pl-4">
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-md">
|
||||
<label class="label">
|
||||
<span class="label-text">Dockerfile path</span>
|
||||
</label>
|
||||
<%= bc_form.text_field(
|
||||
:dockerfile_path,
|
||||
class: "input input-bordered w-full focus:outline-offset-0",
|
||||
value: build_configuration.dockerfile_path
|
||||
) %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">* Required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<div>
|
||||
<div>
|
||||
<% build_configuration = project.build_configuration || BuildConfiguration.new(driver: 'docker') %>
|
||||
<% build_configuration = project.build_configuration || BuildConfiguration.new %>
|
||||
<%= form.fields_for :build_configuration, build_configuration do |bc_form| %>
|
||||
<div class="form-control mt-4">
|
||||
<%= render "shared/partials/radio_selector", selected: build_configuration.driver, options: [
|
||||
@@ -30,7 +30,22 @@
|
||||
].select { |option| BuildConfiguration::BUILDER_OPTIONS.include?(option[:value].to_sym) } %>
|
||||
|
||||
</div>
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-sm">
|
||||
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-md">
|
||||
<label class="label">
|
||||
<span class="label-text">Build context directory</span>
|
||||
</label>
|
||||
<%= bc_form.text_field(
|
||||
:context_directory,
|
||||
class: "input input-bordered w-full focus:outline-offset-0",
|
||||
value: build_configuration.context_directory
|
||||
) %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Where should we run the build? Defaults to the root of the repository.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-md">
|
||||
<label class="label">
|
||||
<span class="label-text">Credentials</span>
|
||||
</label>
|
||||
@@ -49,10 +64,10 @@
|
||||
{ class: "select select-bordered w-full" }
|
||||
) %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Select a provider with a container registry</span>
|
||||
<span class="label-text-alt">Select a credential that gives access to a container registry</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-sm">
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-md">
|
||||
<label class="label">
|
||||
<span class="label-text">Image repository</span>
|
||||
</label>
|
||||
@@ -64,6 +79,34 @@
|
||||
pattern: "[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]/[a-zA-Z0-9._-]+",
|
||||
title: "Must be in the format 'namespace/repo'"
|
||||
) %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">If this is left blank, a container registry will be automatically created for you.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control mt-4">
|
||||
<label class="label mb-2">
|
||||
<span class="label-text">Build method</span>
|
||||
</label>
|
||||
<%= render "shared/partials/radio_selector", selected: build_configuration.build_type || "dockerfile", options: [
|
||||
{
|
||||
icon: "skill-icons:docker",
|
||||
name: "project[build_configuration][build_type]",
|
||||
label: "Dockerfile",
|
||||
value: "dockerfile",
|
||||
description: "Build images using a Dockerfile. Traditional Docker build approach with full control over the build process.",
|
||||
partial: "projects/build_configurations/dockerfile_fields",
|
||||
locals: { bc_form:, build_configuration: }
|
||||
},
|
||||
{
|
||||
icon: "devicon:heroku",
|
||||
name: "project[build_configuration][build_type]",
|
||||
label: "Buildpacks",
|
||||
value: "buildpacks",
|
||||
description: "Use Cloud Native Buildpacks to automatically detect and build your application. No Dockerfile needed.",
|
||||
partial: "projects/build_configurations/buildpack_fields",
|
||||
locals: { bc_form:, build_configuration: }
|
||||
}
|
||||
] %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -80,16 +80,6 @@
|
||||
<span class="label-text-alt">* Required</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-1 mb-2 w-full max-w-sm">
|
||||
<label class="label">
|
||||
<span class="label-text">Container registry URL</span>
|
||||
</label>
|
||||
<%= form.text_field :container_registry_url, class: "input input-bordered w-full focus:outline-offset-0", value: "" %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">If this is left blank, <span data-new-project-target="gitProviderLabel">Github</span> Container Registry will be used</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -100,23 +90,6 @@
|
||||
<%= form.check_box :autodeploy, class: "checkbox" %>
|
||||
<% end %>
|
||||
|
||||
<%= render(FormFieldComponent.new(
|
||||
label: "Dockerfile path",
|
||||
description: "The path to the Dockerfile in your repository."
|
||||
)) do %>
|
||||
<%= form.text_field :dockerfile_path, class: "input input-bordered w-full focus:outline-offset-0" %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">* Required</span>
|
||||
</label>
|
||||
<% end %>
|
||||
|
||||
<%= render(FormFieldComponent.new(
|
||||
label: "Docker build context directory",
|
||||
description: "The directory to use as the build context for the Docker build."
|
||||
)) do %>
|
||||
<%= form.text_field :docker_build_context_directory, class: "input input-bordered w-full focus:outline-offset-0" %>
|
||||
<% end %>
|
||||
|
||||
<%= render(FormFieldComponent.new(
|
||||
label: "Predeploy command",
|
||||
description: "The command to run before deploying the project. This is useful for running migrations or other setup commands."
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
<%= render "projects/volumes/index", project: @project %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">Buildpacks</h2>
|
||||
<hr class="mt-3 mb-4 border-t border-base-300" />
|
||||
|
||||
<%= render "build_packs/search" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">Danger zone</h2>
|
||||
<hr class="mt-3 mb-4 border-t border-base-300" />
|
||||
|
||||
@@ -23,32 +23,10 @@
|
||||
<%= form.check_box :autodeploy, class: "checkbox" %>
|
||||
<% end %>
|
||||
|
||||
<%= render(FormFieldComponent.new(label: "Dockerfile path")) do %>
|
||||
<%= form.text_field :dockerfile_path, class: "input input-bordered w-full focus:outline-offset-0" %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">* Required</span>
|
||||
</label>
|
||||
<% end %>
|
||||
|
||||
<%= render(FormFieldComponent.new(label: "Docker build context directory")) do %>
|
||||
<%= form.text_field :docker_build_context_directory, class: "input input-bordered w-full focus:outline-offset-0" %>
|
||||
<% end %>
|
||||
|
||||
<%= render(FormFieldComponent.new(label: "Predeploy command")) do %>
|
||||
<%= form.text_field :predeploy_command, class: "input input-bordered w-full focus:outline-offset-0" %>
|
||||
<% end %>
|
||||
|
||||
<%= render(FormFieldComponent.new(label: "Container registry URL")) do %>
|
||||
<%= form.text_field(
|
||||
:container_registry_url,
|
||||
class: "input input-bordered w-full focus:outline-offset-0",
|
||||
value: form.object.attributes["container_registry_url"],
|
||||
) %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">If this is left blank, <%= project.github? ? "Github" : "Gitlab" %> Container Registry will be used</span>
|
||||
</label>
|
||||
<% end %>
|
||||
|
||||
<% if Flipper.enabled?(:build_configuration, current_account) %>
|
||||
<%= render(FormFieldComponent.new(label: "Build configuration")) do %>
|
||||
<%= render "projects/build_configurations/form", form: form, project: project %>
|
||||
|
||||
@@ -47,6 +47,12 @@ Rails.application.routes.draw do
|
||||
# get "/dashboard", to: "dashboard#show", as: :user_root
|
||||
end
|
||||
get "/integrations/github/repositories", to: "integrations/github/repositories#index"
|
||||
resources :build_packs, only: [] do
|
||||
collection do
|
||||
get :search
|
||||
get :details
|
||||
end
|
||||
end
|
||||
resources :add_ons do
|
||||
collection do
|
||||
get :search
|
||||
|
||||
20
db/migrate/20251102192149_create_build_packs.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class CreateBuildPacks < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :build_packs do |t|
|
||||
t.references :build_configuration, null: false, foreign_key: true
|
||||
t.integer :reference_type, null: false # registry, docker, git, url, path
|
||||
t.string :namespace # for registry buildpacks
|
||||
t.string :name # for registry buildpacks
|
||||
t.string :version
|
||||
t.integer :build_order, null: false
|
||||
t.text :uri # for git, url, path, or docker references
|
||||
t.jsonb :details, default: {}
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :build_packs, [ :build_configuration_id, :reference_type, :namespace, :name ], name: 'index_build_packs_on_config_type_namespace_name'
|
||||
add_index :build_packs, [ :build_configuration_id, :uri ], name: 'index_build_packs_on_config_uri'
|
||||
add_column :build_configurations, :buildpack_base_builder, :string
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
class MoveDockerFieldsFromProjectToBuildConfiguration < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
# Add build fields to build_configurations (excluding docker_command which is used at runtime)
|
||||
add_column :build_configurations, :context_directory, :string, default: "./", null: false
|
||||
add_column :build_configurations, :dockerfile_path, :string, default: "./Dockerfile", null: false
|
||||
add_column :build_configurations, :build_type, :integer
|
||||
|
||||
# Backfill data from projects to build_configurations
|
||||
reversible do |dir|
|
||||
dir.up do
|
||||
Project.reset_column_information
|
||||
BuildConfiguration.reset_column_information
|
||||
|
||||
Project.find_each do |project|
|
||||
# Create build_configuration if it doesn't exist
|
||||
if project.build_configuration.nil?
|
||||
# Skip projects that don't have the required associations
|
||||
next unless project.project_credential_provider.present?
|
||||
|
||||
BuildConfiguration.create!(
|
||||
project: project,
|
||||
provider: project.project_credential_provider.provider,
|
||||
driver: BuildConfiguration::DEFAULT_BUILDER,
|
||||
image_repository: project.repository_url,
|
||||
context_directory: project.docker_build_context_directory,
|
||||
dockerfile_path: project.dockerfile_path,
|
||||
build_type: :dockerfile,
|
||||
)
|
||||
else
|
||||
# Update existing build_configuration
|
||||
project.build_configuration.update!(
|
||||
context_directory: project.docker_build_context_directory,
|
||||
dockerfile_path: project.dockerfile_path,
|
||||
build_type: :dockerfile,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
change_column_null :build_configurations, :build_type, false
|
||||
end
|
||||
|
||||
def down
|
||||
# Remove build fields from build_configurations
|
||||
remove_column :build_configurations, :context_directory
|
||||
remove_column :build_configurations, :dockerfile_path
|
||||
remove_column :build_configurations, :build_type
|
||||
end
|
||||
end
|
||||
4
db/schema.rb
generated
@@ -10,7 +10,11 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
<<<<<<< HEAD
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_11_10_152921) do
|
||||
=======
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_11_03_000229) do
|
||||
>>>>>>> main
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ services:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- "postgres:/var/lib/postgresql/data"
|
||||
networks:
|
||||
- default
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
# command: sleep infinity
|
||||
depends_on:
|
||||
- postgres
|
||||
stdin_open: true
|
||||
@@ -37,6 +37,9 @@ services:
|
||||
ACCOUNT_SIGN_IN_ONLY: "true"
|
||||
volumes:
|
||||
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
networks:
|
||||
- default
|
||||
- portainer_network
|
||||
|
||||
worker:
|
||||
build:
|
||||
@@ -50,6 +53,18 @@ services:
|
||||
LOCAL_MODE: "true"
|
||||
volumes:
|
||||
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
networks:
|
||||
- default
|
||||
- portainer_network
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
|
||||
networks:
|
||||
# default local network for this compose file
|
||||
default:
|
||||
name: canine_default # optional; can omit to let compose auto-name
|
||||
# external network created elsewhere
|
||||
portainer_network:
|
||||
external: true
|
||||
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
"@codemirror/view": "^6.38.1",
|
||||
"@gorails/ninja-keys": "^1.2.1",
|
||||
"@hotwired/stimulus": "^3.2.2",
|
||||
"@hotwired/turbo-rails": "^8.0.10",
|
||||
"@hotwired/turbo-rails": "^8.0.20",
|
||||
"@iconify/tailwind": "^1.1.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@rails/actioncable": "^7.1.0",
|
||||
"@rails/activestorage": "^7.2.100",
|
||||
"@rails/request.js": "^0.0.11",
|
||||
"@rails/request.js": "^0.0.12",
|
||||
"@rails/ujs": "^7.1.3-4",
|
||||
"@stimulus-components/sortable": "^5.0.3",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.0",
|
||||
"@tailwindcss/forms": "^0.5.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
@@ -44,6 +45,7 @@
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||
"sass": "^1.79.3",
|
||||
"sortablejs": "^1.15.6",
|
||||
"stimulus-flatpickr": "^3.0.0-0",
|
||||
"stimulus-textarea-autogrow": "^4.1.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
|
||||
BIN
public/images/languages/buildpack.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/images/languages/golang.webp
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/images/languages/java.webp
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/images/languages/javascript.webp
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
public/images/languages/php.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/images/languages/python.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/images/languages/ruby.webp
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
public/images/languages/rust.webp
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/images/languages/scala.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
101
resources/build_packs/packs.yaml
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
# Flat list of available buildpacks
|
||||
# Key format: namespace/name
|
||||
|
||||
paketo-buildpacks/ruby:
|
||||
namespace: paketo-buildpacks
|
||||
name: ruby
|
||||
image: /images/languages/ruby.webp
|
||||
description: "Paketo Ruby Buildpack - A language family buildpack for building Ruby apps"
|
||||
reference_type: registry
|
||||
|
||||
heroku/ruby:
|
||||
namespace: heroku
|
||||
name: ruby
|
||||
image: /images/languages/ruby.webp
|
||||
description: "Heroku's buildpack for Ruby applications"
|
||||
reference_type: registry
|
||||
|
||||
paketo-buildpacks/python:
|
||||
namespace: paketo-buildpacks
|
||||
name: python
|
||||
image: /images/languages/python.webp
|
||||
description: "Paketo Python Buildpack - A language family buildpack for building Python apps"
|
||||
reference_type: registry
|
||||
|
||||
heroku/python:
|
||||
namespace: heroku
|
||||
name: python
|
||||
image: /images/languages/python.webp
|
||||
description: "Heroku's buildpack for Python applications"
|
||||
reference_type: registry
|
||||
|
||||
paketo-buildpacks/go:
|
||||
namespace: paketo-buildpacks
|
||||
name: go
|
||||
image: /images/languages/golang.webp
|
||||
description: "Paketo Go Buildpack - A language family buildpack for building Go apps"
|
||||
reference_type: registry
|
||||
|
||||
heroku/go:
|
||||
namespace: heroku
|
||||
name: go
|
||||
image: /images/languages/golang.webp
|
||||
description: "Heroku's buildpack for Go applications"
|
||||
reference_type: registry
|
||||
|
||||
paketo-buildpacks/java:
|
||||
namespace: paketo-buildpacks
|
||||
name: java
|
||||
image: /images/languages/java.webp
|
||||
description: "Paketo Java Buildpack - A language family buildpack for building Java apps"
|
||||
reference_type: registry
|
||||
|
||||
heroku/java:
|
||||
namespace: heroku
|
||||
name: java
|
||||
image: /images/languages/java.webp
|
||||
description: "Heroku's buildpack for Java applications"
|
||||
reference_type: registry
|
||||
|
||||
paketo-buildpacks/nodejs:
|
||||
namespace: paketo-buildpacks
|
||||
name: nodejs
|
||||
image: /images/languages/javascript.webp
|
||||
description: "Paketo Node.js Buildpack - A language family buildpack for building Node.js apps"
|
||||
reference_type: registry
|
||||
|
||||
heroku/nodejs:
|
||||
namespace: heroku
|
||||
name: nodejs
|
||||
image: /images/languages/javascript.webp
|
||||
description: "Heroku's buildpack for Node.js applications"
|
||||
reference_type: registry
|
||||
|
||||
paketo-buildpacks/php:
|
||||
namespace: paketo-buildpacks
|
||||
name: php
|
||||
image: /images/languages/php.webp
|
||||
description: "Paketo PHP Buildpack - A language family buildpack for building PHP apps"
|
||||
reference_type: registry
|
||||
|
||||
heroku/php:
|
||||
namespace: heroku
|
||||
name: php
|
||||
image: /images/languages/php.webp
|
||||
description: "Heroku's buildpack for PHP applications"
|
||||
reference_type: registry
|
||||
|
||||
paketo-buildpacks/sbt:
|
||||
namespace: paketo-buildpacks
|
||||
name: sbt
|
||||
image: /images/languages/scala.webp
|
||||
description: "Paketo SBT Buildpack - A buildpack for building Scala apps with SBT"
|
||||
reference_type: registry
|
||||
|
||||
heroku/scala:
|
||||
namespace: heroku
|
||||
name: scala
|
||||
image: /images/languages/scala.webp
|
||||
description: "Heroku's buildpack for Scala applications"
|
||||
reference_type: registry
|
||||
95
spec/actions/buildpacks/details_spec.rb
Normal file
@@ -0,0 +1,95 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Buildpacks::Details do
|
||||
describe '.execute' do
|
||||
let(:namespace) { 'paketo-buildpacks' }
|
||||
let(:name) { 'passenger' }
|
||||
let(:fixture_path) { Rails.root.join('spec/resources/build_packs/details_passenger.json') }
|
||||
let(:fixture_response) { File.read(fixture_path) }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://registry.buildpacks.io/api/v1/buildpacks/#{namespace}/#{name}")
|
||||
.to_return(status: 200, body: fixture_response, headers: { 'Content-Type' => 'application/json' })
|
||||
end
|
||||
|
||||
context 'when the API request is successful' do
|
||||
it 'returns success' do
|
||||
result = described_class.execute(namespace: namespace, name: name)
|
||||
|
||||
expect(result).to be_success
|
||||
end
|
||||
|
||||
it 'returns a BuildpackDetailsResult struct' do
|
||||
result = described_class.execute(namespace: namespace, name: name)
|
||||
|
||||
expect(result.result).to be_a(Buildpacks::Details::BuildpackDetailsResult)
|
||||
end
|
||||
|
||||
it 'parses the latest buildpack information correctly' do
|
||||
result = described_class.execute(namespace: namespace, name: name)
|
||||
latest = result.result.latest
|
||||
|
||||
expect(latest).to be_a(Buildpacks::Details::BuildpackLatestDetails)
|
||||
expect(latest.namespace).to eq('paketo-buildpacks')
|
||||
expect(latest.name).to eq('passenger')
|
||||
expect(latest.version).to be_present
|
||||
expect(latest.description).to include('passenger')
|
||||
expect(latest.homepage).to be_present
|
||||
expect(latest.licenses).to include('Apache-2.0')
|
||||
expect(latest.stacks).to be_an(Array)
|
||||
expect(latest.id).to be_present
|
||||
end
|
||||
|
||||
it 'parses the versions array correctly' do
|
||||
result = described_class.execute(namespace: namespace, name: name)
|
||||
versions = result.result.versions
|
||||
|
||||
expect(versions).to be_an(Array)
|
||||
expect(versions.first).to be_a(Buildpacks::Details::BuildpackVersion)
|
||||
expect(versions.first.version).to be_present
|
||||
expect(versions.first.link).to be_present
|
||||
end
|
||||
|
||||
it 'versions array contains multiple versions' do
|
||||
result = described_class.execute(namespace: namespace, name: name)
|
||||
|
||||
expect(result.result.versions.length).to be > 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the API request fails' do
|
||||
before do
|
||||
stub_request(:get, "https://registry.buildpacks.io/api/v1/buildpacks/#{namespace}/#{name}")
|
||||
.to_return(status: 404, body: 'Not Found')
|
||||
end
|
||||
|
||||
it 'returns failure' do
|
||||
result = described_class.execute(namespace: namespace, name: name)
|
||||
|
||||
expect(result).to be_failure
|
||||
end
|
||||
|
||||
it 'includes an error message' do
|
||||
result = described_class.execute(namespace: namespace, name: name)
|
||||
|
||||
expect(result.message).to include('Failed to fetch buildpack details')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different namespace and name' do
|
||||
let(:namespace) { 'different-namespace' }
|
||||
let(:name) { 'different-buildpack' }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://registry.buildpacks.io/api/v1/buildpacks/#{namespace}/#{name}")
|
||||
.to_return(status: 200, body: fixture_response, headers: { 'Content-Type' => 'application/json' })
|
||||
end
|
||||
|
||||
it 'constructs the correct URL' do
|
||||
described_class.execute(namespace: namespace, name: name)
|
||||
|
||||
expect(WebMock).to have_requested(:get, "https://registry.buildpacks.io/api/v1/buildpacks/#{namespace}/#{name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
77
spec/actions/buildpacks/search_spec.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Buildpacks::Search do
|
||||
describe '.execute' do
|
||||
let(:query) { 'ruby' }
|
||||
let(:fixture_path) { Rails.root.join('spec/resources/build_packs/search_ruby.json') }
|
||||
let(:fixture_response) { File.read(fixture_path) }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://registry.buildpacks.io/api/v1/search")
|
||||
.with(query: { matches: query })
|
||||
.to_return(status: 200, body: fixture_response, headers: { 'Content-Type' => 'application/json' })
|
||||
end
|
||||
|
||||
context 'when the API request is successful' do
|
||||
it 'returns success' do
|
||||
result = described_class.execute(query: query)
|
||||
|
||||
expect(result).to be_success
|
||||
end
|
||||
|
||||
it 'returns an array of BuildpackResult structs' do
|
||||
result = described_class.execute(query: query)
|
||||
|
||||
expect(result.results).to be_an(Array)
|
||||
expect(result.results.first).to be_a(Buildpacks::Search::BuildpackResult)
|
||||
end
|
||||
|
||||
it 'parses the latest buildpack information correctly' do
|
||||
result = described_class.execute(query: query)
|
||||
first_result = result.results.first
|
||||
|
||||
expect(first_result.latest).to be_a(Buildpacks::Search::BuildpackLatest)
|
||||
expect(first_result.latest.namespace).to eq('paketo-buildpacks')
|
||||
expect(first_result.latest.name).to eq('ruby')
|
||||
expect(first_result.latest.description).to include('Ruby')
|
||||
expect(first_result.latest.licenses).to include('Apache-2.0')
|
||||
end
|
||||
|
||||
it 'parses the versions array correctly' do
|
||||
result = described_class.execute(query: query)
|
||||
first_result = result.results.first
|
||||
|
||||
expect(first_result.versions).to be_an(Array)
|
||||
expect(first_result.versions.first).to be_a(Buildpacks::Search::BuildpackVersion)
|
||||
expect(first_result.versions.first.version).to be_present
|
||||
expect(first_result.versions.first.link).to be_present
|
||||
end
|
||||
|
||||
it 'handles multiple search results' do
|
||||
result = described_class.execute(query: query)
|
||||
|
||||
expect(result.results.length).to be > 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the API request fails' do
|
||||
before do
|
||||
stub_request(:get, "https://registry.buildpacks.io/api/v1/search")
|
||||
.with(query: { matches: query })
|
||||
.to_return(status: 500, body: 'Internal Server Error')
|
||||
end
|
||||
|
||||
it 'returns failure' do
|
||||
result = described_class.execute(query: query)
|
||||
|
||||
expect(result).to be_failure
|
||||
end
|
||||
|
||||
it 'includes an error message' do
|
||||
result = described_class.execute(query: query)
|
||||
|
||||
expect(result.message).to include('Failed to search buildpacks')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -72,6 +72,68 @@ RSpec.describe Projects::Create do
|
||||
expect(subject.project.build_configuration.provider_id).to eq(provider.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with buildpacks' do
|
||||
include_context 'buildpack details stubbing'
|
||||
let(:params) do
|
||||
ActionController::Parameters.new({
|
||||
project: {
|
||||
name: 'example-repo',
|
||||
branch: 'main',
|
||||
cluster_id: cluster.id,
|
||||
repository_url: 'example/repo',
|
||||
docker_command: 'rails s',
|
||||
container_registry_url: '',
|
||||
project_credential_provider: {
|
||||
provider_id: provider.id
|
||||
},
|
||||
build_configuration: {
|
||||
driver: 'docker',
|
||||
image_repository: 'example/repo',
|
||||
provider_id: provider.id,
|
||||
build_type: 'buildpacks',
|
||||
buildpack_base_builder: 'paketobuildpacks/builder:full',
|
||||
build_packs_attributes: [
|
||||
{
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'ruby',
|
||||
version: '',
|
||||
reference_type: 'registry'
|
||||
},
|
||||
{
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'nodejs',
|
||||
version: '1.2.3',
|
||||
reference_type: 'registry'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
it 'creates build packs associated with build configuration' do
|
||||
expect(subject).to be_success
|
||||
expect(subject.project.build_configuration).to be_persisted
|
||||
expect(subject.project.build_configuration.build_type).to eq('buildpacks')
|
||||
expect(subject.project.build_configuration.buildpack_base_builder).to eq('paketobuildpacks/builder:full')
|
||||
|
||||
build_packs = subject.project.build_configuration.build_packs
|
||||
expect(build_packs.count).to eq(2)
|
||||
|
||||
first_pack = build_packs.first
|
||||
expect(first_pack.namespace).to eq('paketo-buildpacks')
|
||||
expect(first_pack.name).to eq('ruby')
|
||||
expect(first_pack.version).to eq('')
|
||||
expect(first_pack.reference_type).to eq('registry')
|
||||
|
||||
second_pack = build_packs.second
|
||||
expect(second_pack.namespace).to eq('paketo-buildpacks')
|
||||
expect(second_pack.name).to eq('nodejs')
|
||||
expect(second_pack.version).to eq('1.2.3')
|
||||
expect(second_pack.reference_type).to eq('registry')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for docker hub' do
|
||||
@@ -95,6 +157,7 @@ RSpec.describe Projects::Create do
|
||||
expect(subject).to eq([
|
||||
Projects::ValidateGitRepository,
|
||||
Projects::ValidateNamespaceAvailability,
|
||||
Projects::InitializeBuildPacks,
|
||||
Projects::Save,
|
||||
Projects::RegisterGitWebhook
|
||||
])
|
||||
@@ -110,6 +173,7 @@ RSpec.describe Projects::Create do
|
||||
expect(subject).to eq([
|
||||
Projects::ValidateGitRepository,
|
||||
Projects::ValidateNamespaceAvailability,
|
||||
Projects::InitializeBuildPacks,
|
||||
Projects::Save
|
||||
])
|
||||
end
|
||||
|
||||
58
spec/actions/projects/initialize_build_packs_spec.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Projects::InitializeBuildPacks do
|
||||
include_context 'buildpack details stubbing'
|
||||
|
||||
let(:provider) { create(:provider, :github) }
|
||||
let(:project) { create(:project) }
|
||||
let(:build_configuration) do
|
||||
create(:build_configuration,
|
||||
project: project,
|
||||
provider: provider,
|
||||
build_type: :buildpacks,
|
||||
buildpack_base_builder: 'paketobuildpacks/builder:full')
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
ActionController::Parameters.new({
|
||||
project: {
|
||||
build_configuration: {
|
||||
build_packs_attributes: [
|
||||
{
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'ruby',
|
||||
version: '0.47.7',
|
||||
reference_type: 'registry'
|
||||
},
|
||||
{
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'nodejs',
|
||||
version: '',
|
||||
reference_type: 'registry'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
subject { described_class.execute(build_configuration: build_configuration, params: params) }
|
||||
|
||||
it 'builds build packs from attributes' do
|
||||
expect { subject }.to change { build_configuration.build_packs.size }.from(0).to(2)
|
||||
|
||||
first_pack = build_configuration.build_packs[0]
|
||||
expect(first_pack.namespace).to eq('paketo-buildpacks')
|
||||
expect(first_pack.name).to eq('ruby')
|
||||
expect(first_pack.version).to eq('0.47.7')
|
||||
expect(first_pack.reference_type).to eq('registry')
|
||||
|
||||
second_pack = build_configuration.build_packs[1]
|
||||
expect(second_pack.namespace).to eq('paketo-buildpacks')
|
||||
expect(second_pack.name).to eq('nodejs')
|
||||
expect(second_pack.version).to eq('')
|
||||
expect(second_pack.reference_type).to eq('registry')
|
||||
end
|
||||
end
|
||||
76
spec/actions/projects/update_build_packs_spec.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Projects::UpdateBuildPacks do
|
||||
include_context 'buildpack details stubbing'
|
||||
|
||||
let(:provider) { create(:provider, :github) }
|
||||
let(:project) { create(:project) }
|
||||
let(:build_configuration) do
|
||||
create(:build_configuration,
|
||||
project: project,
|
||||
provider: provider,
|
||||
build_type: :buildpacks,
|
||||
buildpack_base_builder: 'paketobuildpacks/builder:full')
|
||||
end
|
||||
|
||||
let!(:existing_ruby) do
|
||||
create(
|
||||
:build_pack,
|
||||
build_configuration: build_configuration,
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'ruby',
|
||||
reference_type: 'registry',
|
||||
build_order: 0,
|
||||
)
|
||||
end
|
||||
|
||||
let!(:existing_nodejs) do
|
||||
create(
|
||||
:build_pack,
|
||||
build_configuration: build_configuration,
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'nodejs',
|
||||
reference_type: 'registry',
|
||||
build_order: 1,
|
||||
)
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
ActionController::Parameters.new({
|
||||
project: {
|
||||
build_configuration: {
|
||||
build_packs_attributes: [
|
||||
{
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'go',
|
||||
version: '',
|
||||
reference_type: 'registry'
|
||||
},
|
||||
{
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'ruby',
|
||||
version: '',
|
||||
reference_type: 'registry'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
let(:context) do
|
||||
{
|
||||
build_configuration: build_configuration,
|
||||
params: params
|
||||
}
|
||||
end
|
||||
|
||||
it 'keeps existing build packs, creates new ones, and deletes missing ones' do
|
||||
expect(build_configuration.build_packs.map(&:key)).to eq([ 'paketo-buildpacks/ruby', 'paketo-buildpacks/nodejs' ])
|
||||
described_class.execute(context)
|
||||
build_configuration.build_packs.reload
|
||||
expect(build_configuration.build_packs.map(&:key)).to eq([ 'paketo-buildpacks/go', 'paketo-buildpacks/ruby' ])
|
||||
end
|
||||
end
|
||||
@@ -9,9 +9,7 @@ RSpec.describe Projects::Update do
|
||||
name: 'original-name',
|
||||
branch: 'main',
|
||||
cluster: cluster,
|
||||
docker_build_context_directory: '.',
|
||||
repository_url: 'original/repo',
|
||||
dockerfile_path: 'Dockerfile',
|
||||
)
|
||||
end
|
||||
|
||||
@@ -23,9 +21,11 @@ RSpec.describe Projects::Update do
|
||||
name: 'updated-name',
|
||||
branch: 'develop',
|
||||
cluster_id: cluster.id,
|
||||
docker_build_context_directory: './app',
|
||||
repository_url: 'updated/repo',
|
||||
dockerfile_path: 'docker/Dockerfile'
|
||||
build_configuration: {
|
||||
context_directory: './app',
|
||||
dockerfile_path: 'docker/Dockerfile'
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
@@ -37,9 +37,10 @@ RSpec.describe Projects::Update do
|
||||
expect(result).to be_success
|
||||
expect(result.project.name).to eq('updated-name')
|
||||
expect(result.project.branch).to eq('develop')
|
||||
expect(result.project.docker_build_context_directory).to eq('./app')
|
||||
expect(result.project.build_configuration.context_directory).to eq('./app')
|
||||
expect(result.project.repository_url).to eq('updated/repo')
|
||||
expect(result.project.dockerfile_path).to eq('docker/Dockerfile')
|
||||
expect(result.project.docker_command).to eq('bundle exec rails s')
|
||||
expect(result.project.build_configuration.dockerfile_path).to eq('docker/Dockerfile')
|
||||
end
|
||||
|
||||
it 'strips and downcases repository_url' do
|
||||
@@ -61,7 +62,9 @@ RSpec.describe Projects::Update do
|
||||
driver: 'k8s',
|
||||
build_cloud_id: build_cloud.id,
|
||||
provider_id: build_provider.id,
|
||||
image_repository: 'updated/repo'
|
||||
image_repository: 'updated/repo',
|
||||
context_directory: './app',
|
||||
dockerfile_path: 'docker/Dockerfile'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -189,5 +192,72 @@ RSpec.describe Projects::Update do
|
||||
expect(project.reload.name).to eq(original_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with buildpacks' do
|
||||
include_context 'buildpack details stubbing'
|
||||
let(:build_provider) { create(:provider, :container_registry, user:) }
|
||||
let!(:existing_build_config) do
|
||||
create(:build_configuration,
|
||||
project: project,
|
||||
driver: 'docker',
|
||||
provider: build_provider,
|
||||
build_type: 'buildpacks',
|
||||
buildpack_base_builder: 'paketobuildpacks/builder:full'
|
||||
)
|
||||
end
|
||||
|
||||
let!(:old_build_pack) do
|
||||
create(:build_pack,
|
||||
build_configuration: existing_build_config,
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'python',
|
||||
version: '1.0.0',
|
||||
build_order: 0
|
||||
)
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
ActionController::Parameters.new({
|
||||
project: {
|
||||
name: 'updated-name',
|
||||
build_configuration: {
|
||||
build_type: 'buildpacks',
|
||||
buildpack_base_builder: 'paketobuildpacks/builder:full',
|
||||
build_packs_attributes: [
|
||||
{
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'ruby',
|
||||
version: '0.47.7',
|
||||
reference_type: 'registry'
|
||||
},
|
||||
{
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'nodejs',
|
||||
version: '1.2.3',
|
||||
reference_type: 'registry'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
subject { described_class.call(project, params) }
|
||||
|
||||
it 'updates build packs with correct order and removes old packs' do
|
||||
expect { subject }.to change { BuildPack.count }.by(1)
|
||||
|
||||
result = subject
|
||||
expect(result).to be_success
|
||||
|
||||
build_packs = project.build_configuration.reload.build_packs
|
||||
expect(build_packs.count).to eq(2)
|
||||
expect(build_packs.map(&:name)).not_to include('python')
|
||||
expect(build_packs.first.name).to eq('ruby')
|
||||
expect(build_packs.first.build_order).to eq(0)
|
||||
expect(build_packs.second.name).to eq('nodejs')
|
||||
expect(build_packs.second.build_order).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,6 +32,9 @@ FactoryBot.define do
|
||||
provider
|
||||
project
|
||||
driver { :docker }
|
||||
build_type { :dockerfile }
|
||||
image_repository { "CanineHQ/canine" }
|
||||
context_directory { "." }
|
||||
dockerfile_path { "./Dockerfile" }
|
||||
end
|
||||
end
|
||||
|
||||
62
spec/factories/build_packs.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: build_packs
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# build_order :integer not null
|
||||
# details :jsonb
|
||||
# name :string
|
||||
# namespace :string
|
||||
# reference_type :integer not null
|
||||
# uri :text
|
||||
# version :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# build_configuration_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_build_packs_on_build_configuration_id (build_configuration_id)
|
||||
# index_build_packs_on_config_type_namespace_name (build_configuration_id,reference_type,namespace,name)
|
||||
# index_build_packs_on_config_uri (build_configuration_id,uri)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (build_configuration_id => build_configurations.id)
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :build_pack do
|
||||
build_configuration
|
||||
reference_type { "registry" }
|
||||
namespace { "paketo-buildpacks" }
|
||||
name { "ruby" }
|
||||
version { "0.47.7" }
|
||||
sequence(:build_order) { |n| n }
|
||||
details do
|
||||
{
|
||||
"description" => "A language family buildpack for building Ruby apps",
|
||||
"homepage" => "https://github.com/paketo-buildpacks/ruby",
|
||||
"licenses" => [ "Apache-2.0" ],
|
||||
"stacks" => [ "io.buildpacks.stacks.bionic", "io.buildpacks.stacks.jammy" ]
|
||||
}
|
||||
end
|
||||
|
||||
trait :git do
|
||||
reference_type { "git" }
|
||||
namespace { nil }
|
||||
name { nil }
|
||||
version { nil }
|
||||
uri { "https://github.com/DataDog/heroku-buildpack-datadog.git" }
|
||||
details { {} }
|
||||
end
|
||||
|
||||
trait :url do
|
||||
reference_type { "url" }
|
||||
namespace { nil }
|
||||
name { nil }
|
||||
version { nil }
|
||||
uri { "https://github.com/heroku/buildpacks-ruby/releases/download/v0.1.0/buildpack.tgz" }
|
||||
details { {} }
|
||||
end
|
||||
end
|
||||
end
|
||||
96
spec/models/build_pack_spec.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: build_packs
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# build_order :integer not null
|
||||
# details :jsonb
|
||||
# name :string
|
||||
# namespace :string
|
||||
# reference_type :integer not null
|
||||
# uri :text
|
||||
# version :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# build_configuration_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_build_packs_on_build_configuration_id (build_configuration_id)
|
||||
# index_build_packs_on_config_type_namespace_name (build_configuration_id,reference_type,namespace,name)
|
||||
# index_build_packs_on_config_uri (build_configuration_id,uri)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (build_configuration_id => build_configurations.id)
|
||||
#
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe BuildPack, type: :model do
|
||||
describe '#reference' do
|
||||
context 'registry buildpack' do
|
||||
let(:build_pack) { create(:build_pack) }
|
||||
|
||||
context 'when version is present' do
|
||||
before { build_pack.update(version: '0.47.7') }
|
||||
|
||||
it 'returns namespace/name:version' do
|
||||
expect(build_pack.reference).to eq('paketo-buildpacks/ruby:0.47.7')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when version is not present' do
|
||||
before { build_pack.update(version: nil) }
|
||||
|
||||
it 'returns namespace/name' do
|
||||
expect(build_pack.reference).to eq('paketo-buildpacks/ruby')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'git buildpack' do
|
||||
let(:build_pack) { create(:build_pack, :git) }
|
||||
|
||||
it 'returns the git URL' do
|
||||
expect(build_pack.reference).to eq('https://github.com/DataDog/heroku-buildpack-datadog.git')
|
||||
end
|
||||
end
|
||||
|
||||
context 'url buildpack' do
|
||||
let(:build_pack) { create(:build_pack, :url) }
|
||||
|
||||
it 'returns the URL' do
|
||||
expect(build_pack.reference).to eq('https://github.com/heroku/buildpacks-ruby/releases/download/v0.1.0/buildpack.tgz')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#verified?' do
|
||||
it 'returns true for registry buildpacks with verified namespaces' do
|
||||
build_pack = create(:build_pack, namespace: 'paketo-buildpacks')
|
||||
expect(build_pack.verified?).to be true
|
||||
end
|
||||
|
||||
it 'returns false for registry buildpacks with unverified namespaces' do
|
||||
build_pack = create(:build_pack, namespace: 'custom-buildpacks')
|
||||
expect(build_pack.verified?).to be false
|
||||
end
|
||||
|
||||
it 'returns false for git buildpacks' do
|
||||
build_pack = create(:build_pack, :git)
|
||||
expect(build_pack.verified?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#display_name' do
|
||||
it 'returns namespace/name for registry buildpacks' do
|
||||
build_pack = create(:build_pack)
|
||||
expect(build_pack.display_name).to eq('paketo-buildpacks/ruby')
|
||||
end
|
||||
|
||||
it 'returns repo name for git buildpacks' do
|
||||
build_pack = create(:build_pack, :git)
|
||||
expect(build_pack.display_name).to eq('heroku-buildpack-datadog')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -24,7 +24,7 @@ require 'support/webmock'
|
||||
# directory. Alternatively, in the individual `*_spec.rb` files, manually
|
||||
# require only the support files necessary.
|
||||
#
|
||||
# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
|
||||
Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
|
||||
|
||||
# Checks for pending migrations and applies them before tests are run.
|
||||
# If you are not using ActiveRecord, you can remove these lines.
|
||||
|
||||
1
spec/resources/build_packs/details_passenger.json
Normal file
1
spec/resources/build_packs/search_ruby.json
Normal file
21
spec/support/shared_contexts/buildpack_details_stubbing.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'buildpack details stubbing' do
|
||||
let(:buildpack_details_result) do
|
||||
double(
|
||||
result: Buildpacks::Details::BuildpackDetailsResult.new(
|
||||
latest: {
|
||||
version: '0.47.7',
|
||||
namespace: 'paketo-buildpacks',
|
||||
name: 'go',
|
||||
description: 'Go buildpack'
|
||||
},
|
||||
versions: []
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Buildpacks::Details).to receive(:execute).and_return(buildpack_details_result)
|
||||
end
|
||||
end
|
||||
252
yarn.lock
@@ -97,11 +97,126 @@
|
||||
style-mod "^4.1.0"
|
||||
w3c-keyname "^2.2.4"
|
||||
|
||||
"@esbuild/aix-ppc64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c"
|
||||
integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==
|
||||
|
||||
"@esbuild/android-arm64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0"
|
||||
integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==
|
||||
|
||||
"@esbuild/android-arm@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810"
|
||||
integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==
|
||||
|
||||
"@esbuild/android-x64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705"
|
||||
integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==
|
||||
|
||||
"@esbuild/darwin-arm64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz"
|
||||
integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==
|
||||
|
||||
"@esbuild/darwin-x64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107"
|
||||
integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7"
|
||||
integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==
|
||||
|
||||
"@esbuild/freebsd-x64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93"
|
||||
integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==
|
||||
|
||||
"@esbuild/linux-arm64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75"
|
||||
integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==
|
||||
|
||||
"@esbuild/linux-arm@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d"
|
||||
integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==
|
||||
|
||||
"@esbuild/linux-ia32@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb"
|
||||
integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==
|
||||
|
||||
"@esbuild/linux-loong64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c"
|
||||
integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==
|
||||
|
||||
"@esbuild/linux-mips64el@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3"
|
||||
integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==
|
||||
|
||||
"@esbuild/linux-ppc64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e"
|
||||
integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==
|
||||
|
||||
"@esbuild/linux-riscv64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25"
|
||||
integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==
|
||||
|
||||
"@esbuild/linux-s390x@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319"
|
||||
integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==
|
||||
|
||||
"@esbuild/linux-x64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef"
|
||||
integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==
|
||||
|
||||
"@esbuild/netbsd-x64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c"
|
||||
integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2"
|
||||
integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==
|
||||
|
||||
"@esbuild/openbsd-x64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf"
|
||||
integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==
|
||||
|
||||
"@esbuild/sunos-x64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4"
|
||||
integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==
|
||||
|
||||
"@esbuild/win32-arm64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b"
|
||||
integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==
|
||||
|
||||
"@esbuild/win32-ia32@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103"
|
||||
integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==
|
||||
|
||||
"@esbuild/win32-x64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244"
|
||||
integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==
|
||||
|
||||
"@gorails/ninja-keys@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/@gorails/ninja-keys/-/ninja-keys-1.2.1.tgz"
|
||||
@@ -111,23 +226,23 @@
|
||||
hotkeys-js "3.8.9"
|
||||
lit "2.2.2"
|
||||
|
||||
"@hotwired/stimulus@^3.0.0", "@hotwired/stimulus@^3.2.1", "@hotwired/stimulus@^3.2.2", "@hotwired/stimulus@>= 3.0.0":
|
||||
"@hotwired/stimulus@^3.0.0", "@hotwired/stimulus@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz"
|
||||
integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==
|
||||
|
||||
"@hotwired/turbo-rails@^8.0.10":
|
||||
version "8.0.10"
|
||||
resolved "https://registry.npmjs.org/@hotwired/turbo-rails/-/turbo-rails-8.0.10.tgz"
|
||||
integrity sha512-BkERfjTbNwMb9/YQi0RL9+f9zkD+dZH2klEONtGwXrIE3O9BE1937Nn9++koZpDryD4XN3zE5U5ibyWoYJAWBg==
|
||||
"@hotwired/turbo-rails@^8.0.20":
|
||||
version "8.0.20"
|
||||
resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-8.0.20.tgz#a6f6f78591e9868ca1e5e67f4c7d453dbd49a475"
|
||||
integrity sha512-4aYkYF9XMKL7ZZPfgElq15+60osZOwMwhztE4myKQYEzCPvaPUxwZH301tOrBNtWUwOD+TNOm1Hrpeaq22RX9A==
|
||||
dependencies:
|
||||
"@hotwired/turbo" "^8.0.6"
|
||||
"@rails/actioncable" "^7.0"
|
||||
"@hotwired/turbo" "^8.0.20"
|
||||
"@rails/actioncable" ">=7.0"
|
||||
|
||||
"@hotwired/turbo@^8.0.6":
|
||||
version "8.0.10"
|
||||
resolved "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.10.tgz"
|
||||
integrity sha512-xen1YhNQirAHlA8vr/444XsTNITC1Il2l/Vx4w8hAWPpI5nQO78mVHNsmFuayETodzPwh25ob2TgfCEV/Loiog==
|
||||
"@hotwired/turbo@^8.0.20":
|
||||
version "8.0.20"
|
||||
resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.20.tgz#068ede648c4db09fed4cf0ac0266788056673f2f"
|
||||
integrity sha512-IilkH/+h92BRLeY/rMMR3MUh1gshIfdra/qZzp/Bl5FmiALD/6sQZK/ecxSbumeyOYiWr/JRI+Au1YQmkJGnoA==
|
||||
|
||||
"@iconify/tailwind@^1.1.3":
|
||||
version "1.1.3"
|
||||
@@ -251,7 +366,7 @@
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
@@ -286,7 +401,12 @@
|
||||
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||
|
||||
"@rails/actioncable@^7.0", "@rails/actioncable@^7.1.0":
|
||||
"@rails/actioncable@>=7.0":
|
||||
version "8.1.100"
|
||||
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-8.1.100.tgz#b1f85f3482425fb91e5eba1deb55152ee0bb2f85"
|
||||
integrity sha512-j4vJQqz51CDVYv2UafKRu4jiZi5/gTnm7NkyL+VMIgEw3s8jtVtmzu9uItUaZccUg9NJ6o05yVyBAHxNfTuCRA==
|
||||
|
||||
"@rails/actioncable@^7.1.0":
|
||||
version "7.2.100"
|
||||
resolved "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.2.100.tgz"
|
||||
integrity sha512-7xtIENf0Yw59AFDM3+xqxPCZxev3QVAqjPmUzmgsB9eL8S/zTpB0IU9srNc7XknzJI4e09XKNnCaJRx3gfYzXA==
|
||||
@@ -298,10 +418,10 @@
|
||||
dependencies:
|
||||
spark-md5 "^3.0.1"
|
||||
|
||||
"@rails/request.js@^0.0.11":
|
||||
version "0.0.11"
|
||||
resolved "https://registry.npmjs.org/@rails/request.js/-/request.js-0.0.11.tgz"
|
||||
integrity sha512-2U3uYS0kbljt+pAstN+LIlZOl7xmOKig5N6FrvtUWO1wq0zR1Hf90fHfD2SYiyV8yH1nyKpoTmbLqWT0xe1zDg==
|
||||
"@rails/request.js@^0.0.12":
|
||||
version "0.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@rails/request.js/-/request.js-0.0.12.tgz#3d1f73e7585141d9c4c2149a34476d128eb900bc"
|
||||
integrity sha512-g3//JBja1s04Zflj7IoMLQuXza9i4ZvtLmm0r0dMwh1QQUs6rL2iKUOGGyERfLsd81SnXC5ucfVV//rtsDlEEA==
|
||||
|
||||
"@rails/ujs@^7.1.3-4":
|
||||
version "7.1.3-4"
|
||||
@@ -313,6 +433,11 @@
|
||||
resolved "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz"
|
||||
integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==
|
||||
|
||||
"@stimulus-components/sortable@^5.0.3":
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@stimulus-components/sortable/-/sortable-5.0.3.tgz#b8b64f954a05497fa6ca00f21557d466a2ebe47b"
|
||||
integrity sha512-GSiu4CX2irR3gIuo3nsSNXlJsEvXVurC2caNTNshlGbsP0sHb70bLn7iiVib7iGSmLJdXdU6wCpD3AEnMKCt2Q==
|
||||
|
||||
"@tailwindcss/aspect-ratio@^0.4.0":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz"
|
||||
@@ -447,7 +572,7 @@ braces@^3.0.3, braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.23.3, "browserslist@>= 4.21.0":
|
||||
browserslist@^4.23.3:
|
||||
version "4.24.0"
|
||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz"
|
||||
integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==
|
||||
@@ -467,7 +592,7 @@ caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663:
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz"
|
||||
integrity sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==
|
||||
|
||||
chart.js@^4.4.6, chart.js@>=2.8.0, chart.js@4:
|
||||
chart.js@4, chart.js@^4.4.6:
|
||||
version "4.4.6"
|
||||
resolved "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz"
|
||||
integrity sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==
|
||||
@@ -488,37 +613,7 @@ chartkick@^5.0.1:
|
||||
chartjs-adapter-date-fns ">=3"
|
||||
date-fns ">=2"
|
||||
|
||||
chokidar@^3.3.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
|
||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^3.5.2:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
|
||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^3.5.3:
|
||||
chokidar@^3.3.0, chokidar@^3.5.2, chokidar@^3.5.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
|
||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||
@@ -635,7 +730,7 @@ daisyui@^4.12.10:
|
||||
picocolors "^1"
|
||||
postcss-js "^4"
|
||||
|
||||
date-fns@>=2, date-fns@>=2.0.0:
|
||||
date-fns@>=2:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz"
|
||||
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
|
||||
@@ -699,7 +794,7 @@ esbuild-rails@^1.0.7:
|
||||
dependencies:
|
||||
fast-glob "^3.2.12"
|
||||
|
||||
esbuild@*, esbuild@^0.24.0:
|
||||
esbuild@^0.24.0:
|
||||
version "0.24.0"
|
||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz"
|
||||
integrity sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==
|
||||
@@ -764,7 +859,7 @@ fill-range@^7.1.1:
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
flatpickr@^4.6.10, flatpickr@>=4.6.2:
|
||||
flatpickr@^4.6.10:
|
||||
version "4.6.13"
|
||||
resolved "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz"
|
||||
integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==
|
||||
@@ -943,7 +1038,7 @@ jackspeak@^3.1.2:
|
||||
optionalDependencies:
|
||||
"@pkgjs/parseargs" "^0.11.0"
|
||||
|
||||
jiti@^1.21.0, jiti@>=1.21.0:
|
||||
jiti@^1.21.0:
|
||||
version "1.21.6"
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz"
|
||||
integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==
|
||||
@@ -1000,15 +1095,6 @@ lit-html@^2.2.0, lit-html@^2.8.0:
|
||||
dependencies:
|
||||
"@types/trusted-types" "^2.0.2"
|
||||
|
||||
lit@^2.0.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz"
|
||||
integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==
|
||||
dependencies:
|
||||
"@lit/reactive-element" "^1.6.0"
|
||||
lit-element "^3.3.0"
|
||||
lit-html "^2.8.0"
|
||||
|
||||
lit@2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.npmjs.org/lit/-/lit-2.2.2.tgz"
|
||||
@@ -1018,6 +1104,15 @@ lit@2.2.2:
|
||||
lit-element "^3.2.0"
|
||||
lit-html "^2.2.0"
|
||||
|
||||
lit@^2.0.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz"
|
||||
integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==
|
||||
dependencies:
|
||||
"@lit/reactive-element" "^1.6.0"
|
||||
lit-element "^3.3.0"
|
||||
lit-html "^2.8.0"
|
||||
|
||||
local-time@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/local-time/-/local-time-3.0.2.tgz"
|
||||
@@ -1270,14 +1365,6 @@ postcss-reporter@^7.0.0:
|
||||
picocolors "^1.0.0"
|
||||
thenby "^1.3.4"
|
||||
|
||||
postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.1.1:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz"
|
||||
integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-selector-parser@6.0.10:
|
||||
version "6.0.10"
|
||||
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz"
|
||||
@@ -1286,12 +1373,20 @@ postcss-selector-parser@6.0.10:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.1.1:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz"
|
||||
integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.47, postcss@>=8.0.9:
|
||||
postcss@^8.4.23, postcss@^8.4.47:
|
||||
version "8.4.47"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz"
|
||||
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
|
||||
@@ -1305,7 +1400,7 @@ prettier-plugin-tailwindcss@^0.6.8:
|
||||
resolved "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz"
|
||||
integrity sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==
|
||||
|
||||
prettier@^3.0, prettier@^3.3.3:
|
||||
prettier@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz"
|
||||
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
|
||||
@@ -1423,7 +1518,12 @@ slash@^5.0.0, slash@^5.1.0:
|
||||
resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz"
|
||||
integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==
|
||||
|
||||
source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0":
|
||||
sortablejs@^1.15.6:
|
||||
version "1.15.6"
|
||||
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.6.tgz#ff93699493f5b8ab8d828f933227b4988df1d393"
|
||||
integrity sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
@@ -1544,7 +1644,7 @@ svg.filter.js@^2.0.2:
|
||||
dependencies:
|
||||
svg.js "^2.2.5"
|
||||
|
||||
svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5, svg.js@>=2.3.x:
|
||||
svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz"
|
||||
integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==
|
||||
@@ -1583,7 +1683,7 @@ tailwindcss-stimulus-components@^5.1.0:
|
||||
resolved "https://registry.npmjs.org/tailwindcss-stimulus-components/-/tailwindcss-stimulus-components-5.1.1.tgz"
|
||||
integrity sha512-9e3H9WLZoGzWQBVOdYvxqM42MsJ4dfkfdrs3NGV+HYvHbIcF9tukl2MPKVZpBv/pP+puZbJv0lFgdn79IGVApA==
|
||||
|
||||
tailwindcss@^3.4.1, "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", "tailwindcss@>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20", "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20":
|
||||
tailwindcss@^3.4.1:
|
||||
version "3.4.13"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz"
|
||||
integrity sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==
|
||||
|
||||