warp_engine: CiRepository -> Pipeline rename everywhere, ci_ service prefixes dropped, unknown platform allowed
This commit is contained in:
@@ -60,11 +60,11 @@ ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do
|
||||
end
|
||||
|
||||
member_action :rotate, method: :post do
|
||||
result = WarpEngine::CiSecretSyncService.new.rotate(resource)
|
||||
result = WarpEngine::SecretSyncService.new.rotate(resource)
|
||||
if result[:rotated]
|
||||
session[:warp_engine_plain_token] = result[:new_token].plain_token
|
||||
redirect_to resource_path(result[:new_token]),
|
||||
notice: "Token rotated and synced to #{result.dig(:sync_result, :synced)&.size || 0} repo(s)"
|
||||
notice: "Token rotated and synced to #{result.dig(:sync_result, :synced)&.size || 0} pipeline(s)"
|
||||
else
|
||||
redirect_to resource_path(resource),
|
||||
alert: "Rotation failed: #{result[:reason]}"
|
||||
@@ -101,13 +101,13 @@ ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do
|
||||
session[:warp_engine_plain_token] = resource.plain_token
|
||||
|
||||
if WarpEngine.woodpecker_configured?
|
||||
service = WarpEngine::CiSecretSyncService.new
|
||||
repos = service.repos_for_token(resource)
|
||||
if repos.any?
|
||||
result = service.provision(resource.plain_token, repos: repos)
|
||||
flash[:notice] = "Token created and synced to #{result[:synced].size} repo(s)."
|
||||
service = WarpEngine::SecretSyncService.new
|
||||
pipelines = service.pipelines_for_token(resource)
|
||||
if pipelines.any?
|
||||
result = service.provision(resource.plain_token, pipelines: pipelines)
|
||||
flash[:notice] = "Token created and synced to #{result[:synced].size} pipeline(s)."
|
||||
if result[:failed].any?
|
||||
flash[:alert] = "Failed to sync to #{result[:failed].size} repo(s)."
|
||||
flash[:alert] = "Failed to sync to #{result[:failed].size} pipeline(s)."
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -124,7 +124,7 @@ ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do
|
||||
|
||||
def destroy
|
||||
if WarpEngine.woodpecker_configured?
|
||||
WarpEngine::CiSecretSyncService.new.deprovision(resource)
|
||||
WarpEngine::SecretSyncService.new.deprovision(resource)
|
||||
end
|
||||
resource.revoke!
|
||||
redirect_to collection_path, notice: "Token revoked and Woodpecker secrets cleaned up."
|
||||
|
||||
+10
-10
@@ -1,4 +1,4 @@
|
||||
ActiveAdmin.register WarpEngine::CiRepository, as: "Pipeline" do
|
||||
ActiveAdmin.register WarpEngine::Pipeline, as: "Pipeline" do
|
||||
actions :index, :show, :edit, :update
|
||||
|
||||
menu parent: "🌀 WarpEngine", priority: 10, label: "🚀 Pipelines"
|
||||
@@ -30,9 +30,9 @@ ActiveAdmin.register WarpEngine::CiRepository, as: "Pipeline" do
|
||||
end
|
||||
}
|
||||
column :last_pipeline_at
|
||||
actions defaults: true do |repo|
|
||||
if repo.active && WarpEngine.woodpecker_configured?
|
||||
item "Trigger", trigger_admin_pipeline_path(repo), method: :post, class: "member_link"
|
||||
actions defaults: true do |pipeline|
|
||||
if pipeline.active && WarpEngine.woodpecker_configured?
|
||||
item "Trigger", trigger_admin_pipeline_path(pipeline), method: :post, class: "member_link"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -76,7 +76,7 @@ ActiveAdmin.register WarpEngine::CiRepository, as: "Pipeline" do
|
||||
para "This repository is inactive.", style: "color:#999;"
|
||||
else
|
||||
begin
|
||||
pipelines = WarpEngine::CiPipelineService.new.list_pipelines(resource, page: 1)
|
||||
pipelines = WarpEngine::PipelineService.new.list_pipelines(resource, page: 1)
|
||||
if pipelines.is_a?(Array) && pipelines.any?
|
||||
table_for pipelines.first(10) do
|
||||
column("Number") { |p| p["number"] }
|
||||
@@ -97,15 +97,15 @@ ActiveAdmin.register WarpEngine::CiRepository, as: "Pipeline" do
|
||||
end
|
||||
|
||||
member_action :trigger, method: :post do
|
||||
repo = WarpEngine::CiRepository.find(params[:id])
|
||||
WarpEngine::CiPipelineService.new.trigger(repo)
|
||||
redirect_to resource_path(repo), notice: "Pipeline triggered for #{repo.full_name}"
|
||||
pipeline = WarpEngine::Pipeline.find(params[:id])
|
||||
WarpEngine::PipelineService.new.trigger(pipeline)
|
||||
redirect_to resource_path(pipeline), notice: "Pipeline triggered for #{pipeline.full_name}"
|
||||
rescue => e
|
||||
redirect_to resource_path(repo), alert: "Trigger failed: #{e.message}"
|
||||
redirect_to resource_path(pipeline), alert: "Trigger failed: #{e.message}"
|
||||
end
|
||||
|
||||
collection_action :sync, method: :post do
|
||||
result = WarpEngine::CiRepoSyncService.new.sync_all
|
||||
result = WarpEngine::PipelineSyncService.new.sync_all
|
||||
redirect_to collection_path,
|
||||
notice: "Synced: #{result[:created].size} new, #{result[:updated].size} updated, #{result[:deactivated].size} deactivated"
|
||||
rescue => e
|
||||
|
||||
@@ -53,9 +53,9 @@ ActiveAdmin.register WarpEngine::Software, as: "Software" do
|
||||
end
|
||||
end
|
||||
|
||||
if resource.ci_repository
|
||||
if resource.pipeline
|
||||
div style: "margin-bottom:8px;" do
|
||||
a href: admin_pipeline_path(resource.ci_repository), style: "display:inline-flex;align-items:center;gap:6px;font-weight:bold;color:#5850ec;" do
|
||||
a href: admin_pipeline_path(resource.pipeline), style: "display:inline-flex;align-items:center;gap:6px;font-weight:bold;color:#5850ec;" do
|
||||
span "🚀", style: "font-size:16px;"
|
||||
text_node "Pipelines"
|
||||
end
|
||||
|
||||
@@ -6,36 +6,36 @@ module WarpEngine
|
||||
before_action :require_woodpecker!
|
||||
|
||||
resource_description do
|
||||
short "CI repository and pipeline management"
|
||||
short "CI pipeline management"
|
||||
end
|
||||
|
||||
api :GET, "/api/ci/repos", "List active CI repositories"
|
||||
returns code: 200, desc: "JSON array of tracked repos"
|
||||
api :GET, "/api/ci/pipelines", "List active pipelines"
|
||||
returns code: 200, desc: "JSON array of tracked pipelines"
|
||||
error code: 503, desc: "Woodpecker not configured"
|
||||
def repos
|
||||
records = CiRepository.active.includes(:software)
|
||||
render json: records.map { |r| repo_json(r) }
|
||||
def pipelines
|
||||
records = Pipeline.active.includes(:software)
|
||||
render json: records.map { |p| pipeline_json(p) }
|
||||
end
|
||||
|
||||
api :GET, "/api/ci/repos/:id/status", "Get a CI repository with its latest pipeline"
|
||||
param :id, :number, required: true, desc: "CiRepository id"
|
||||
returns code: 200, desc: "JSON with repo and pipeline data"
|
||||
api :GET, "/api/ci/pipelines/:id/status", "Get a pipeline with its latest run"
|
||||
param :id, :number, required: true, desc: "Pipeline id"
|
||||
returns code: 200, desc: "JSON with pipeline and latest run data"
|
||||
error code: 503, desc: "Woodpecker not configured"
|
||||
def status
|
||||
repo = CiRepository.find(params[:id])
|
||||
pipeline = begin
|
||||
CiPipelineService.new.pipeline_detail(repo, "latest")
|
||||
pipeline = Pipeline.find(params[:id])
|
||||
latest_run = begin
|
||||
PipelineService.new.pipeline_detail(pipeline, "latest")
|
||||
rescue WoodpeckerClient::ApiError
|
||||
nil
|
||||
end
|
||||
render json: { repo: repo_json(repo), pipeline: pipeline }
|
||||
render json: { pipeline: pipeline_json(pipeline), latest_run: latest_run }
|
||||
end
|
||||
|
||||
api :POST, "/api/ci/repos/:id/trigger", "Trigger a pipeline for a CI repository"
|
||||
api :POST, "/api/ci/pipelines/:id/trigger", "Trigger a pipeline"
|
||||
header "X-Update-Secret", "Shared secret or application token (update scope)", required: true
|
||||
param :id, :number, required: true, desc: "CiRepository id"
|
||||
param :id, :number, required: true, desc: "Pipeline id"
|
||||
param :branch, String, required: false, desc: "Branch to build (default: main)"
|
||||
returns code: 200, desc: "JSON with triggered pipeline data"
|
||||
returns code: 200, desc: "JSON with triggered run data"
|
||||
error code: 401, desc: "Invalid secret"
|
||||
error code: 503, desc: "Woodpecker not configured"
|
||||
def trigger
|
||||
@@ -43,8 +43,8 @@ module WarpEngine
|
||||
return render json: { error: "Unauthorized" }, status: :unauthorized
|
||||
end
|
||||
|
||||
repo = CiRepository.find(params[:id])
|
||||
result = CiPipelineService.new.trigger(repo, branch: params[:branch] || "main")
|
||||
pipeline = Pipeline.find(params[:id])
|
||||
result = PipelineService.new.trigger(pipeline, branch: params[:branch] || "main")
|
||||
render json: { triggered: true, pipeline: result }
|
||||
end
|
||||
|
||||
@@ -56,17 +56,17 @@ module WarpEngine
|
||||
render json: { error: "Woodpecker not configured" }, status: :service_unavailable
|
||||
end
|
||||
|
||||
def repo_json(repo)
|
||||
def pipeline_json(pipeline)
|
||||
{
|
||||
id: repo.id,
|
||||
woodpecker_repo_id: repo.woodpecker_repo_id,
|
||||
repo_owner: repo.repo_owner,
|
||||
repo_name: repo.repo_name,
|
||||
platform: repo.platform,
|
||||
active: repo.active,
|
||||
software_name: repo.software&.name,
|
||||
last_pipeline_status: repo.last_pipeline_status,
|
||||
last_pipeline_at: repo.last_pipeline_at
|
||||
id: pipeline.id,
|
||||
woodpecker_repo_id: pipeline.woodpecker_repo_id,
|
||||
repo_owner: pipeline.repo_owner,
|
||||
repo_name: pipeline.repo_name,
|
||||
platform: pipeline.platform,
|
||||
active: pipeline.active,
|
||||
software_name: pipeline.software&.name,
|
||||
last_pipeline_status: pipeline.last_pipeline_status,
|
||||
last_pipeline_at: pipeline.last_pipeline_at
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
module WarpEngine
|
||||
class CiRepoSyncJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
CiRepoSyncService.new.sync_all
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
module WarpEngine
|
||||
class PipelineSyncJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
PipelineSyncService.new.sync_all
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,10 @@
|
||||
module WarpEngine
|
||||
class CiRepository < ApplicationRecord
|
||||
self.table_name = "ci_repositories"
|
||||
class Pipeline < ApplicationRecord
|
||||
self.table_name = "pipelines"
|
||||
|
||||
# Repos synced from Woodpecker without a matching Software land here
|
||||
# until a platform is assigned by hand.
|
||||
UNKNOWN_PLATFORM = "unknown".freeze
|
||||
|
||||
belongs_to :software, class_name: "WarpEngine::Software", optional: true
|
||||
|
||||
@@ -10,7 +14,7 @@ module WarpEngine
|
||||
validates :repo_owner, presence: true
|
||||
validates :repo_name, presence: true
|
||||
validates :platform, presence: true,
|
||||
inclusion: { in: WarpEngine::PlatformLink::SUPPORTED_PLATFORMS }
|
||||
inclusion: { in: WarpEngine::PlatformLink::SUPPORTED_PLATFORMS + [ UNKNOWN_PLATFORM ] }
|
||||
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
@@ -27,6 +31,6 @@ module WarpEngine
|
||||
%w[software]
|
||||
end
|
||||
|
||||
ActiveSupport.run_load_hooks(:warp_engine_ci_repository, self)
|
||||
ActiveSupport.run_load_hooks(:warp_engine_pipeline, self)
|
||||
end
|
||||
end
|
||||
@@ -11,7 +11,7 @@ module WarpEngine
|
||||
has_many :releases, foreign_key: :software_id
|
||||
has_many :downloads, through: :releases
|
||||
has_many :external_links, foreign_key: :software_id
|
||||
has_one :ci_repository, foreign_key: :software_id
|
||||
has_one :pipeline, foreign_key: :software_id
|
||||
|
||||
accepts_nested_attributes_for :software_images, allow_destroy: true
|
||||
accepts_nested_attributes_for :external_links, allow_destroy: true
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
module WarpEngine
|
||||
class CiPipelineService
|
||||
def initialize(client: WoodpeckerClient.new)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def trigger(repo, branch: "main")
|
||||
@client.trigger_pipeline(repo.woodpecker_repo_id, branch: branch)
|
||||
end
|
||||
|
||||
# Fetches one page of a repo's pipelines; the newest one refreshes the
|
||||
# repo's cached last_pipeline_* columns (Pipelines index and the CI API).
|
||||
def list_pipelines(repo, page: 1)
|
||||
pipelines = @client.list_pipelines(repo.woodpecker_repo_id, page: page)
|
||||
refresh_last_pipeline(repo, pipelines.first) if page == 1 && pipelines.is_a?(Array)
|
||||
pipelines
|
||||
end
|
||||
|
||||
def pipeline_detail(repo, number)
|
||||
@client.get_pipeline(repo.woodpecker_repo_id, number)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Woodpecker returns unix epoch seconds in "created".
|
||||
def refresh_last_pipeline(repo, pipeline)
|
||||
return unless pipeline && repo.persisted?
|
||||
|
||||
repo.update_columns(
|
||||
last_pipeline_status: pipeline["status"],
|
||||
last_pipeline_at: pipeline["created"] ? Time.zone.at(pipeline["created"]) : nil
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
module WarpEngine
|
||||
class PipelineService
|
||||
def initialize(client: WoodpeckerClient.new)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def trigger(pipeline, branch: "main")
|
||||
@client.trigger_pipeline(pipeline.woodpecker_repo_id, branch: branch)
|
||||
end
|
||||
|
||||
# Fetches one page of a pipeline's runs; the newest one refreshes the
|
||||
# cached last_pipeline_* columns (Pipelines index and the CI API).
|
||||
def list_pipelines(pipeline, page: 1)
|
||||
runs = @client.list_pipelines(pipeline.woodpecker_repo_id, page: page)
|
||||
refresh_last_pipeline(pipeline, runs.first) if page == 1 && runs.is_a?(Array)
|
||||
runs
|
||||
end
|
||||
|
||||
def pipeline_detail(pipeline, number)
|
||||
@client.get_pipeline(pipeline.woodpecker_repo_id, number)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Woodpecker returns unix epoch seconds in "created".
|
||||
def refresh_last_pipeline(pipeline, run)
|
||||
return unless run && pipeline.persisted?
|
||||
|
||||
pipeline.update_columns(
|
||||
last_pipeline_status: run["status"],
|
||||
last_pipeline_at: run["created"] ? Time.zone.at(run["created"]) : nil
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
module WarpEngine
|
||||
class CiRepoSyncService
|
||||
class PipelineSyncService
|
||||
def initialize(client: WoodpeckerClient.new)
|
||||
@client = client
|
||||
end
|
||||
@@ -10,7 +10,7 @@ module WarpEngine
|
||||
remote_ids = remote_repos.map { |r| r["id"] }
|
||||
|
||||
remote_repos.each do |remote|
|
||||
record = CiRepository.unscoped.find_or_initialize_by(
|
||||
record = Pipeline.unscoped.find_or_initialize_by(
|
||||
woodpecker_repo_id: remote["id"]
|
||||
)
|
||||
|
||||
@@ -22,9 +22,9 @@ module WarpEngine
|
||||
deleted_at: nil
|
||||
)
|
||||
|
||||
if record.platform.blank? || record.platform == "unknown"
|
||||
if record.platform.blank? || record.platform == Pipeline::UNKNOWN_PLATFORM
|
||||
sw = Software.find_by(name: remote["name"])
|
||||
record.platform = sw&.platform || "unknown"
|
||||
record.platform = sw&.platform || Pipeline::UNKNOWN_PLATFORM
|
||||
record.software = sw if sw
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ module WarpEngine
|
||||
results[was_new ? :created : :updated] << record
|
||||
end
|
||||
|
||||
CiRepository.where.not(woodpecker_repo_id: remote_ids).find_each do |orphan|
|
||||
Pipeline.where.not(woodpecker_repo_id: remote_ids).find_each do |orphan|
|
||||
orphan.update!(active: false) if orphan.active?
|
||||
results[:deactivated] << orphan
|
||||
end
|
||||
@@ -48,7 +48,7 @@ module WarpEngine
|
||||
|
||||
def deactivate(repo_id)
|
||||
@client.deactivate_repo(repo_id)
|
||||
record = CiRepository.find_by!(woodpecker_repo_id: repo_id)
|
||||
record = Pipeline.find_by!(woodpecker_repo_id: repo_id)
|
||||
record.update!(active: false)
|
||||
end
|
||||
|
||||
@@ -56,14 +56,14 @@ module WarpEngine
|
||||
|
||||
def sync_single(repo_id)
|
||||
remote = @client.get_repo(repo_id)
|
||||
record = CiRepository.unscoped.find_or_initialize_by(woodpecker_repo_id: repo_id)
|
||||
record = Pipeline.unscoped.find_or_initialize_by(woodpecker_repo_id: repo_id)
|
||||
record.assign_attributes(
|
||||
repo_name: remote["name"], repo_owner: remote["owner"],
|
||||
active: remote["active"], deleted_at: nil
|
||||
)
|
||||
if record.platform.blank?
|
||||
sw = Software.find_by(name: remote["name"])
|
||||
record.platform = sw&.platform || "unknown"
|
||||
record.platform = sw&.platform || Pipeline::UNKNOWN_PLATFORM
|
||||
record.software = sw if sw
|
||||
end
|
||||
record.save!
|
||||
+19
-19
@@ -1,40 +1,40 @@
|
||||
module WarpEngine
|
||||
class CiSecretSyncService
|
||||
class SecretSyncService
|
||||
SECRET_NAME = "application_token".freeze
|
||||
|
||||
def initialize(client: WoodpeckerClient.new)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def provision(plain_token, repos:)
|
||||
def provision(plain_token, pipelines:)
|
||||
results = { synced: [], failed: [] }
|
||||
|
||||
repos.each do |repo|
|
||||
if secret_exists?(repo.woodpecker_repo_id)
|
||||
@client.update_secret(repo.woodpecker_repo_id, SECRET_NAME, value: plain_token)
|
||||
pipelines.each do |pipeline|
|
||||
if secret_exists?(pipeline.woodpecker_repo_id)
|
||||
@client.update_secret(pipeline.woodpecker_repo_id, SECRET_NAME, value: plain_token)
|
||||
else
|
||||
@client.create_secret(repo.woodpecker_repo_id, name: SECRET_NAME, value: plain_token)
|
||||
@client.create_secret(pipeline.woodpecker_repo_id, name: SECRET_NAME, value: plain_token)
|
||||
end
|
||||
results[:synced] << repo
|
||||
results[:synced] << pipeline
|
||||
rescue WoodpeckerClient::ApiError, WoodpeckerClient::ConnectionError => e
|
||||
Rails.logger.error("[CiSecretSyncService] failed for #{repo.full_name}: #{e.message}")
|
||||
results[:failed] << { repo: repo, error: e.message }
|
||||
Rails.logger.error("[SecretSyncService] failed for #{pipeline.full_name}: #{e.message}")
|
||||
results[:failed] << { pipeline: pipeline, error: e.message }
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
def deprovision(application_token)
|
||||
repos_for_token(application_token).each do |repo|
|
||||
@client.delete_secret(repo.woodpecker_repo_id, SECRET_NAME)
|
||||
pipelines_for_token(application_token).each do |pipeline|
|
||||
@client.delete_secret(pipeline.woodpecker_repo_id, SECRET_NAME)
|
||||
rescue WoodpeckerClient::ApiError => e
|
||||
Rails.logger.warn("[CiSecretSyncService] delete failed for #{repo.full_name}: #{e.message}")
|
||||
Rails.logger.warn("[SecretSyncService] delete failed for #{pipeline.full_name}: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
def rotate(application_token)
|
||||
repos = repos_for_token(application_token)
|
||||
return { rotated: false, reason: "no repos" } if repos.empty?
|
||||
pipelines = pipelines_for_token(application_token)
|
||||
return { rotated: false, reason: "no pipelines" } if pipelines.empty?
|
||||
|
||||
new_token = ApplicationToken.create!(
|
||||
name: "#{application_token.name} (rotated #{Date.current})",
|
||||
@@ -45,26 +45,26 @@ module WarpEngine
|
||||
unrestricted: application_token.unrestricted?
|
||||
)
|
||||
|
||||
result = provision(new_token.plain_token, repos: repos)
|
||||
result = provision(new_token.plain_token, pipelines: pipelines)
|
||||
|
||||
if result[:synced].any?
|
||||
application_token.revoke!
|
||||
{ rotated: true, new_token: new_token, sync_result: result }
|
||||
else
|
||||
new_token.revoke!
|
||||
{ rotated: false, reason: "all repos failed", sync_result: result }
|
||||
{ rotated: false, reason: "all pipelines failed", sync_result: result }
|
||||
end
|
||||
end
|
||||
|
||||
def repos_for_token(application_token)
|
||||
def pipelines_for_token(application_token)
|
||||
if application_token.unrestricted?
|
||||
CiRepository.active.to_a
|
||||
Pipeline.active.to_a
|
||||
else
|
||||
software_ids = Software.where(
|
||||
owner_type: application_token.owner_type,
|
||||
owner_id: application_token.owner_id
|
||||
).pluck(:id)
|
||||
CiRepository.active.where(software_id: software_ids).to_a
|
||||
Pipeline.active.where(software_id: software_ids).to_a
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user