warp_engine: CiRepository -> Pipeline rename everywhere, ci_ service prefixes dropped, unknown platform allowed
This commit is contained in:
@@ -305,7 +305,7 @@ c.woodpecker_repo_owner = ENV["WOODPECKER_REPO_OWNER"] # forge org the game repo
|
|||||||
What it unlocks (all surfaced in the admin):
|
What it unlocks (all surfaced in the admin):
|
||||||
|
|
||||||
- **Repo sync** (*Pipelines → Sync from Woodpecker*): mirrors the Woodpecker
|
- **Repo sync** (*Pipelines → Sync from Woodpecker*): mirrors the Woodpecker
|
||||||
repo list into `CiRepository` records, auto-matching each repo to a catalog
|
repo list into `Pipeline` records, auto-matching each repo to a catalog
|
||||||
`Software` by name; repos that disappear from Woodpecker are deactivated.
|
`Software` by name; repos that disappear from Woodpecker are deactivated.
|
||||||
Platform and software links are editable by hand afterwards.
|
Platform and software links are editable by hand afterwards.
|
||||||
- **Pipeline history**: each entry on the *Pipelines* page lists its recent
|
- **Pipeline history**: each entry on the *Pipelines* page lists its recent
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do
|
|||||||
end
|
end
|
||||||
|
|
||||||
member_action :rotate, method: :post do
|
member_action :rotate, method: :post do
|
||||||
result = WarpEngine::CiSecretSyncService.new.rotate(resource)
|
result = WarpEngine::SecretSyncService.new.rotate(resource)
|
||||||
if result[:rotated]
|
if result[:rotated]
|
||||||
session[:warp_engine_plain_token] = result[:new_token].plain_token
|
session[:warp_engine_plain_token] = result[:new_token].plain_token
|
||||||
redirect_to resource_path(result[:new_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
|
else
|
||||||
redirect_to resource_path(resource),
|
redirect_to resource_path(resource),
|
||||||
alert: "Rotation failed: #{result[:reason]}"
|
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
|
session[:warp_engine_plain_token] = resource.plain_token
|
||||||
|
|
||||||
if WarpEngine.woodpecker_configured?
|
if WarpEngine.woodpecker_configured?
|
||||||
service = WarpEngine::CiSecretSyncService.new
|
service = WarpEngine::SecretSyncService.new
|
||||||
repos = service.repos_for_token(resource)
|
pipelines = service.pipelines_for_token(resource)
|
||||||
if repos.any?
|
if pipelines.any?
|
||||||
result = service.provision(resource.plain_token, repos: repos)
|
result = service.provision(resource.plain_token, pipelines: pipelines)
|
||||||
flash[:notice] = "Token created and synced to #{result[:synced].size} repo(s)."
|
flash[:notice] = "Token created and synced to #{result[:synced].size} pipeline(s)."
|
||||||
if result[:failed].any?
|
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
|
end
|
||||||
end
|
end
|
||||||
@@ -124,7 +124,7 @@ ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if WarpEngine.woodpecker_configured?
|
if WarpEngine.woodpecker_configured?
|
||||||
WarpEngine::CiSecretSyncService.new.deprovision(resource)
|
WarpEngine::SecretSyncService.new.deprovision(resource)
|
||||||
end
|
end
|
||||||
resource.revoke!
|
resource.revoke!
|
||||||
redirect_to collection_path, notice: "Token revoked and Woodpecker secrets cleaned up."
|
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
|
actions :index, :show, :edit, :update
|
||||||
|
|
||||||
menu parent: "🌀 WarpEngine", priority: 10, label: "🚀 Pipelines"
|
menu parent: "🌀 WarpEngine", priority: 10, label: "🚀 Pipelines"
|
||||||
@@ -30,9 +30,9 @@ ActiveAdmin.register WarpEngine::CiRepository, as: "Pipeline" do
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
column :last_pipeline_at
|
column :last_pipeline_at
|
||||||
actions defaults: true do |repo|
|
actions defaults: true do |pipeline|
|
||||||
if repo.active && WarpEngine.woodpecker_configured?
|
if pipeline.active && WarpEngine.woodpecker_configured?
|
||||||
item "Trigger", trigger_admin_pipeline_path(repo), method: :post, class: "member_link"
|
item "Trigger", trigger_admin_pipeline_path(pipeline), method: :post, class: "member_link"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -76,7 +76,7 @@ ActiveAdmin.register WarpEngine::CiRepository, as: "Pipeline" do
|
|||||||
para "This repository is inactive.", style: "color:#999;"
|
para "This repository is inactive.", style: "color:#999;"
|
||||||
else
|
else
|
||||||
begin
|
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?
|
if pipelines.is_a?(Array) && pipelines.any?
|
||||||
table_for pipelines.first(10) do
|
table_for pipelines.first(10) do
|
||||||
column("Number") { |p| p["number"] }
|
column("Number") { |p| p["number"] }
|
||||||
@@ -97,15 +97,15 @@ ActiveAdmin.register WarpEngine::CiRepository, as: "Pipeline" do
|
|||||||
end
|
end
|
||||||
|
|
||||||
member_action :trigger, method: :post do
|
member_action :trigger, method: :post do
|
||||||
repo = WarpEngine::CiRepository.find(params[:id])
|
pipeline = WarpEngine::Pipeline.find(params[:id])
|
||||||
WarpEngine::CiPipelineService.new.trigger(repo)
|
WarpEngine::PipelineService.new.trigger(pipeline)
|
||||||
redirect_to resource_path(repo), notice: "Pipeline triggered for #{repo.full_name}"
|
redirect_to resource_path(pipeline), notice: "Pipeline triggered for #{pipeline.full_name}"
|
||||||
rescue => e
|
rescue => e
|
||||||
redirect_to resource_path(repo), alert: "Trigger failed: #{e.message}"
|
redirect_to resource_path(pipeline), alert: "Trigger failed: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
collection_action :sync, method: :post do
|
collection_action :sync, method: :post do
|
||||||
result = WarpEngine::CiRepoSyncService.new.sync_all
|
result = WarpEngine::PipelineSyncService.new.sync_all
|
||||||
redirect_to collection_path,
|
redirect_to collection_path,
|
||||||
notice: "Synced: #{result[:created].size} new, #{result[:updated].size} updated, #{result[:deactivated].size} deactivated"
|
notice: "Synced: #{result[:created].size} new, #{result[:updated].size} updated, #{result[:deactivated].size} deactivated"
|
||||||
rescue => e
|
rescue => e
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ ActiveAdmin.register WarpEngine::Software, as: "Software" do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if resource.ci_repository
|
if resource.pipeline
|
||||||
div style: "margin-bottom:8px;" do
|
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;"
|
span "🚀", style: "font-size:16px;"
|
||||||
text_node "Pipelines"
|
text_node "Pipelines"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,36 +6,36 @@ module WarpEngine
|
|||||||
before_action :require_woodpecker!
|
before_action :require_woodpecker!
|
||||||
|
|
||||||
resource_description do
|
resource_description do
|
||||||
short "CI repository and pipeline management"
|
short "CI pipeline management"
|
||||||
end
|
end
|
||||||
|
|
||||||
api :GET, "/api/ci/repos", "List active CI repositories"
|
api :GET, "/api/ci/pipelines", "List active pipelines"
|
||||||
returns code: 200, desc: "JSON array of tracked repos"
|
returns code: 200, desc: "JSON array of tracked pipelines"
|
||||||
error code: 503, desc: "Woodpecker not configured"
|
error code: 503, desc: "Woodpecker not configured"
|
||||||
def repos
|
def pipelines
|
||||||
records = CiRepository.active.includes(:software)
|
records = Pipeline.active.includes(:software)
|
||||||
render json: records.map { |r| repo_json(r) }
|
render json: records.map { |p| pipeline_json(p) }
|
||||||
end
|
end
|
||||||
|
|
||||||
api :GET, "/api/ci/repos/:id/status", "Get a CI repository with its latest pipeline"
|
api :GET, "/api/ci/pipelines/:id/status", "Get a pipeline with its latest run"
|
||||||
param :id, :number, required: true, desc: "CiRepository id"
|
param :id, :number, required: true, desc: "Pipeline id"
|
||||||
returns code: 200, desc: "JSON with repo and pipeline data"
|
returns code: 200, desc: "JSON with pipeline and latest run data"
|
||||||
error code: 503, desc: "Woodpecker not configured"
|
error code: 503, desc: "Woodpecker not configured"
|
||||||
def status
|
def status
|
||||||
repo = CiRepository.find(params[:id])
|
pipeline = Pipeline.find(params[:id])
|
||||||
pipeline = begin
|
latest_run = begin
|
||||||
CiPipelineService.new.pipeline_detail(repo, "latest")
|
PipelineService.new.pipeline_detail(pipeline, "latest")
|
||||||
rescue WoodpeckerClient::ApiError
|
rescue WoodpeckerClient::ApiError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
render json: { repo: repo_json(repo), pipeline: pipeline }
|
render json: { pipeline: pipeline_json(pipeline), latest_run: latest_run }
|
||||||
end
|
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
|
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)"
|
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: 401, desc: "Invalid secret"
|
||||||
error code: 503, desc: "Woodpecker not configured"
|
error code: 503, desc: "Woodpecker not configured"
|
||||||
def trigger
|
def trigger
|
||||||
@@ -43,8 +43,8 @@ module WarpEngine
|
|||||||
return render json: { error: "Unauthorized" }, status: :unauthorized
|
return render json: { error: "Unauthorized" }, status: :unauthorized
|
||||||
end
|
end
|
||||||
|
|
||||||
repo = CiRepository.find(params[:id])
|
pipeline = Pipeline.find(params[:id])
|
||||||
result = CiPipelineService.new.trigger(repo, branch: params[:branch] || "main")
|
result = PipelineService.new.trigger(pipeline, branch: params[:branch] || "main")
|
||||||
render json: { triggered: true, pipeline: result }
|
render json: { triggered: true, pipeline: result }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,17 +56,17 @@ module WarpEngine
|
|||||||
render json: { error: "Woodpecker not configured" }, status: :service_unavailable
|
render json: { error: "Woodpecker not configured" }, status: :service_unavailable
|
||||||
end
|
end
|
||||||
|
|
||||||
def repo_json(repo)
|
def pipeline_json(pipeline)
|
||||||
{
|
{
|
||||||
id: repo.id,
|
id: pipeline.id,
|
||||||
woodpecker_repo_id: repo.woodpecker_repo_id,
|
woodpecker_repo_id: pipeline.woodpecker_repo_id,
|
||||||
repo_owner: repo.repo_owner,
|
repo_owner: pipeline.repo_owner,
|
||||||
repo_name: repo.repo_name,
|
repo_name: pipeline.repo_name,
|
||||||
platform: repo.platform,
|
platform: pipeline.platform,
|
||||||
active: repo.active,
|
active: pipeline.active,
|
||||||
software_name: repo.software&.name,
|
software_name: pipeline.software&.name,
|
||||||
last_pipeline_status: repo.last_pipeline_status,
|
last_pipeline_status: pipeline.last_pipeline_status,
|
||||||
last_pipeline_at: repo.last_pipeline_at
|
last_pipeline_at: pipeline.last_pipeline_at
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
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
|
module WarpEngine
|
||||||
class CiRepository < ApplicationRecord
|
class Pipeline < ApplicationRecord
|
||||||
self.table_name = "ci_repositories"
|
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
|
belongs_to :software, class_name: "WarpEngine::Software", optional: true
|
||||||
|
|
||||||
@@ -10,7 +14,7 @@ module WarpEngine
|
|||||||
validates :repo_owner, presence: true
|
validates :repo_owner, presence: true
|
||||||
validates :repo_name, presence: true
|
validates :repo_name, presence: true
|
||||||
validates :platform, 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) }
|
scope :active, -> { where(active: true) }
|
||||||
|
|
||||||
@@ -27,6 +31,6 @@ module WarpEngine
|
|||||||
%w[software]
|
%w[software]
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveSupport.run_load_hooks(:warp_engine_ci_repository, self)
|
ActiveSupport.run_load_hooks(:warp_engine_pipeline, self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -11,7 +11,7 @@ module WarpEngine
|
|||||||
has_many :releases, foreign_key: :software_id
|
has_many :releases, foreign_key: :software_id
|
||||||
has_many :downloads, through: :releases
|
has_many :downloads, through: :releases
|
||||||
has_many :external_links, foreign_key: :software_id
|
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 :software_images, allow_destroy: true
|
||||||
accepts_nested_attributes_for :external_links, 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
|
module WarpEngine
|
||||||
class CiRepoSyncService
|
class PipelineSyncService
|
||||||
def initialize(client: WoodpeckerClient.new)
|
def initialize(client: WoodpeckerClient.new)
|
||||||
@client = client
|
@client = client
|
||||||
end
|
end
|
||||||
@@ -10,7 +10,7 @@ module WarpEngine
|
|||||||
remote_ids = remote_repos.map { |r| r["id"] }
|
remote_ids = remote_repos.map { |r| r["id"] }
|
||||||
|
|
||||||
remote_repos.each do |remote|
|
remote_repos.each do |remote|
|
||||||
record = CiRepository.unscoped.find_or_initialize_by(
|
record = Pipeline.unscoped.find_or_initialize_by(
|
||||||
woodpecker_repo_id: remote["id"]
|
woodpecker_repo_id: remote["id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,9 +22,9 @@ module WarpEngine
|
|||||||
deleted_at: nil
|
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"])
|
sw = Software.find_by(name: remote["name"])
|
||||||
record.platform = sw&.platform || "unknown"
|
record.platform = sw&.platform || Pipeline::UNKNOWN_PLATFORM
|
||||||
record.software = sw if sw
|
record.software = sw if sw
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ module WarpEngine
|
|||||||
results[was_new ? :created : :updated] << record
|
results[was_new ? :created : :updated] << record
|
||||||
end
|
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?
|
orphan.update!(active: false) if orphan.active?
|
||||||
results[:deactivated] << orphan
|
results[:deactivated] << orphan
|
||||||
end
|
end
|
||||||
@@ -48,7 +48,7 @@ module WarpEngine
|
|||||||
|
|
||||||
def deactivate(repo_id)
|
def deactivate(repo_id)
|
||||||
@client.deactivate_repo(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)
|
record.update!(active: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,14 +56,14 @@ module WarpEngine
|
|||||||
|
|
||||||
def sync_single(repo_id)
|
def sync_single(repo_id)
|
||||||
remote = @client.get_repo(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(
|
record.assign_attributes(
|
||||||
repo_name: remote["name"], repo_owner: remote["owner"],
|
repo_name: remote["name"], repo_owner: remote["owner"],
|
||||||
active: remote["active"], deleted_at: nil
|
active: remote["active"], deleted_at: nil
|
||||||
)
|
)
|
||||||
if record.platform.blank?
|
if record.platform.blank?
|
||||||
sw = Software.find_by(name: remote["name"])
|
sw = Software.find_by(name: remote["name"])
|
||||||
record.platform = sw&.platform || "unknown"
|
record.platform = sw&.platform || Pipeline::UNKNOWN_PLATFORM
|
||||||
record.software = sw if sw
|
record.software = sw if sw
|
||||||
end
|
end
|
||||||
record.save!
|
record.save!
|
||||||
+19
-19
@@ -1,40 +1,40 @@
|
|||||||
module WarpEngine
|
module WarpEngine
|
||||||
class CiSecretSyncService
|
class SecretSyncService
|
||||||
SECRET_NAME = "application_token".freeze
|
SECRET_NAME = "application_token".freeze
|
||||||
|
|
||||||
def initialize(client: WoodpeckerClient.new)
|
def initialize(client: WoodpeckerClient.new)
|
||||||
@client = client
|
@client = client
|
||||||
end
|
end
|
||||||
|
|
||||||
def provision(plain_token, repos:)
|
def provision(plain_token, pipelines:)
|
||||||
results = { synced: [], failed: [] }
|
results = { synced: [], failed: [] }
|
||||||
|
|
||||||
repos.each do |repo|
|
pipelines.each do |pipeline|
|
||||||
if secret_exists?(repo.woodpecker_repo_id)
|
if secret_exists?(pipeline.woodpecker_repo_id)
|
||||||
@client.update_secret(repo.woodpecker_repo_id, SECRET_NAME, value: plain_token)
|
@client.update_secret(pipeline.woodpecker_repo_id, SECRET_NAME, value: plain_token)
|
||||||
else
|
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
|
end
|
||||||
results[:synced] << repo
|
results[:synced] << pipeline
|
||||||
rescue WoodpeckerClient::ApiError, WoodpeckerClient::ConnectionError => e
|
rescue WoodpeckerClient::ApiError, WoodpeckerClient::ConnectionError => e
|
||||||
Rails.logger.error("[CiSecretSyncService] failed for #{repo.full_name}: #{e.message}")
|
Rails.logger.error("[SecretSyncService] failed for #{pipeline.full_name}: #{e.message}")
|
||||||
results[:failed] << { repo: repo, error: e.message }
|
results[:failed] << { pipeline: pipeline, error: e.message }
|
||||||
end
|
end
|
||||||
|
|
||||||
results
|
results
|
||||||
end
|
end
|
||||||
|
|
||||||
def deprovision(application_token)
|
def deprovision(application_token)
|
||||||
repos_for_token(application_token).each do |repo|
|
pipelines_for_token(application_token).each do |pipeline|
|
||||||
@client.delete_secret(repo.woodpecker_repo_id, SECRET_NAME)
|
@client.delete_secret(pipeline.woodpecker_repo_id, SECRET_NAME)
|
||||||
rescue WoodpeckerClient::ApiError => e
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def rotate(application_token)
|
def rotate(application_token)
|
||||||
repos = repos_for_token(application_token)
|
pipelines = pipelines_for_token(application_token)
|
||||||
return { rotated: false, reason: "no repos" } if repos.empty?
|
return { rotated: false, reason: "no pipelines" } if pipelines.empty?
|
||||||
|
|
||||||
new_token = ApplicationToken.create!(
|
new_token = ApplicationToken.create!(
|
||||||
name: "#{application_token.name} (rotated #{Date.current})",
|
name: "#{application_token.name} (rotated #{Date.current})",
|
||||||
@@ -45,26 +45,26 @@ module WarpEngine
|
|||||||
unrestricted: application_token.unrestricted?
|
unrestricted: application_token.unrestricted?
|
||||||
)
|
)
|
||||||
|
|
||||||
result = provision(new_token.plain_token, repos: repos)
|
result = provision(new_token.plain_token, pipelines: pipelines)
|
||||||
|
|
||||||
if result[:synced].any?
|
if result[:synced].any?
|
||||||
application_token.revoke!
|
application_token.revoke!
|
||||||
{ rotated: true, new_token: new_token, sync_result: result }
|
{ rotated: true, new_token: new_token, sync_result: result }
|
||||||
else
|
else
|
||||||
new_token.revoke!
|
new_token.revoke!
|
||||||
{ rotated: false, reason: "all repos failed", sync_result: result }
|
{ rotated: false, reason: "all pipelines failed", sync_result: result }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def repos_for_token(application_token)
|
def pipelines_for_token(application_token)
|
||||||
if application_token.unrestricted?
|
if application_token.unrestricted?
|
||||||
CiRepository.active.to_a
|
Pipeline.active.to_a
|
||||||
else
|
else
|
||||||
software_ids = Software.where(
|
software_ids = Software.where(
|
||||||
owner_type: application_token.owner_type,
|
owner_type: application_token.owner_type,
|
||||||
owner_id: application_token.owner_id
|
owner_id: application_token.owner_id
|
||||||
).pluck(:id)
|
).pluck(:id)
|
||||||
CiRepository.active.where(software_id: software_ids).to_a
|
Pipeline.active.where(software_id: software_ids).to_a
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
+3
-3
@@ -7,9 +7,9 @@ WarpEngine::Engine.routes.draw do
|
|||||||
get "builds", to: "builds#index"
|
get "builds", to: "builds#index"
|
||||||
get "softwares/:name/builds", to: "software_builds#show"
|
get "softwares/:name/builds", to: "software_builds#show"
|
||||||
|
|
||||||
get "ci/repos", to: "ci#repos"
|
get "ci/pipelines", to: "ci#pipelines"
|
||||||
get "ci/repos/:id/status", to: "ci#status"
|
get "ci/pipelines/:id/status", to: "ci#status"
|
||||||
post "ci/repos/:id/trigger", to: "ci#trigger"
|
post "ci/pipelines/:id/trigger", to: "ci#trigger"
|
||||||
end
|
end
|
||||||
|
|
||||||
post "build/upload", to: "build/uploads#create"
|
post "build/upload", to: "build/uploads#create"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
class RenameCiRepositoriesToPipelines < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
rename_table :ci_repositories, :pipelines
|
||||||
|
|
||||||
|
rename_index :pipelines, "idx_ci_repos_deleted", "idx_pipelines_deleted"
|
||||||
|
rename_index :pipelines, "idx_ci_repos_owner_name", "idx_pipelines_owner_name"
|
||||||
|
rename_index :pipelines, "idx_ci_repos_software", "idx_pipelines_software"
|
||||||
|
rename_index :pipelines, "idx_ci_repos_wp_id", "idx_pipelines_wp_id"
|
||||||
|
|
||||||
|
remove_foreign_key :pipelines, :softwares,
|
||||||
|
column: :software_id, name: "fk_ci_repos_software", on_delete: :nullify
|
||||||
|
add_foreign_key :pipelines, :softwares,
|
||||||
|
column: :software_id, name: "fk_pipelines_software", on_delete: :nullify
|
||||||
|
end
|
||||||
|
end
|
||||||
+20
-20
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2026_08_06_000001) do
|
ActiveRecord::Schema[8.1].define(version: 2026_08_06_000002) do
|
||||||
create_table "application_tokens", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "application_tokens", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: 3
|
t.datetime "created_at", precision: 3
|
||||||
t.datetime "deleted_at", precision: 3
|
t.datetime "deleted_at", precision: 3
|
||||||
@@ -29,24 +29,6 @@ ActiveRecord::Schema[8.1].define(version: 2026_08_06_000001) do
|
|||||||
t.index ["token_digest"], name: "idx_application_tokens_token_digest", unique: true
|
t.index ["token_digest"], name: "idx_application_tokens_token_digest", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "ci_repositories", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.boolean "active", default: true, null: false
|
|
||||||
t.datetime "created_at", precision: 3
|
|
||||||
t.datetime "deleted_at", precision: 3
|
|
||||||
t.datetime "last_pipeline_at", precision: 3
|
|
||||||
t.string "last_pipeline_status", limit: 32
|
|
||||||
t.string "platform", limit: 32, null: false
|
|
||||||
t.string "repo_name", limit: 128, null: false
|
|
||||||
t.string "repo_owner", limit: 128, null: false
|
|
||||||
t.bigint "software_id", unsigned: true
|
|
||||||
t.datetime "updated_at", precision: 3
|
|
||||||
t.bigint "woodpecker_repo_id", null: false, unsigned: true
|
|
||||||
t.index ["deleted_at"], name: "idx_ci_repos_deleted"
|
|
||||||
t.index ["repo_owner", "repo_name"], name: "idx_ci_repos_owner_name", unique: true
|
|
||||||
t.index ["software_id"], name: "idx_ci_repos_software"
|
|
||||||
t.index ["woodpecker_repo_id"], name: "idx_ci_repos_wp_id", unique: true
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "downloads", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "downloads", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: 3
|
t.datetime "created_at", precision: 3
|
||||||
t.datetime "deleted_at", precision: 3
|
t.datetime "deleted_at", precision: 3
|
||||||
@@ -82,6 +64,24 @@ ActiveRecord::Schema[8.1].define(version: 2026_08_06_000001) do
|
|||||||
t.index ["deleted_at"], name: "idx_images_deleted_at"
|
t.index ["deleted_at"], name: "idx_images_deleted_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "pipelines", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.boolean "active", default: true, null: false
|
||||||
|
t.datetime "created_at", precision: 3
|
||||||
|
t.datetime "deleted_at", precision: 3
|
||||||
|
t.datetime "last_pipeline_at", precision: 3
|
||||||
|
t.string "last_pipeline_status", limit: 32
|
||||||
|
t.string "platform", limit: 32, null: false
|
||||||
|
t.string "repo_name", limit: 128, null: false
|
||||||
|
t.string "repo_owner", limit: 128, null: false
|
||||||
|
t.bigint "software_id", unsigned: true
|
||||||
|
t.datetime "updated_at", precision: 3
|
||||||
|
t.bigint "woodpecker_repo_id", null: false, unsigned: true
|
||||||
|
t.index ["deleted_at"], name: "idx_pipelines_deleted"
|
||||||
|
t.index ["repo_owner", "repo_name"], name: "idx_pipelines_owner_name", unique: true
|
||||||
|
t.index ["software_id"], name: "idx_pipelines_software"
|
||||||
|
t.index ["woodpecker_repo_id"], name: "idx_pipelines_wp_id", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "platform_links", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "platform_links", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: 3
|
t.datetime "created_at", precision: 3
|
||||||
t.datetime "deleted_at", precision: 3
|
t.datetime "deleted_at", precision: 3
|
||||||
@@ -155,9 +155,9 @@ ActiveRecord::Schema[8.1].define(version: 2026_08_06_000001) do
|
|||||||
t.datetime "updated_at", precision: 3
|
t.datetime "updated_at", precision: 3
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "ci_repositories", "softwares", name: "fk_ci_repos_software", on_delete: :nullify
|
|
||||||
add_foreign_key "downloads", "releases", name: "fk_downloads_release", on_delete: :nullify
|
add_foreign_key "downloads", "releases", name: "fk_downloads_release", on_delete: :nullify
|
||||||
add_foreign_key "external_links", "softwares", name: "fk_softwares_external_links", on_delete: :cascade
|
add_foreign_key "external_links", "softwares", name: "fk_softwares_external_links", on_delete: :cascade
|
||||||
|
add_foreign_key "pipelines", "softwares", name: "fk_pipelines_software", on_delete: :nullify
|
||||||
add_foreign_key "release_assets", "releases", name: "fk_releases_release_assets", on_delete: :cascade
|
add_foreign_key "release_assets", "releases", name: "fk_releases_release_assets", on_delete: :cascade
|
||||||
add_foreign_key "releases", "softwares", name: "fk_softwares_releases", on_delete: :cascade
|
add_foreign_key "releases", "softwares", name: "fk_softwares_releases", on_delete: :cascade
|
||||||
add_foreign_key "software_images", "images"
|
add_foreign_key "software_images", "images"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :ci_repository, class: "WarpEngine::CiRepository" do
|
factory :pipeline, class: "WarpEngine::Pipeline" do
|
||||||
sequence(:woodpecker_repo_id) { |n| n }
|
sequence(:woodpecker_repo_id) { |n| n }
|
||||||
repo_owner { "testorg" }
|
repo_owner { "testorg" }
|
||||||
sequence(:repo_name) { |n| "game-#{n}" }
|
sequence(:repo_name) { |n| "game-#{n}" }
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe WarpEngine::CiRepository do
|
RSpec.describe WarpEngine::Pipeline do
|
||||||
describe "validations" do
|
describe "validations" do
|
||||||
subject { build(:ci_repository) }
|
subject { build(:pipeline) }
|
||||||
|
|
||||||
it { is_expected.to validate_presence_of(:woodpecker_repo_id) }
|
it { is_expected.to validate_presence_of(:woodpecker_repo_id) }
|
||||||
it { is_expected.to validate_uniqueness_of(:woodpecker_repo_id) }
|
it { is_expected.to validate_uniqueness_of(:woodpecker_repo_id) }
|
||||||
@@ -11,29 +11,34 @@ RSpec.describe WarpEngine::CiRepository do
|
|||||||
it { is_expected.to validate_presence_of(:platform) }
|
it { is_expected.to validate_presence_of(:platform) }
|
||||||
|
|
||||||
it "rejects unsupported platforms" do
|
it "rejects unsupported platforms" do
|
||||||
repo = build(:ci_repository, platform: "amiga")
|
repo = build(:pipeline, platform: "amiga")
|
||||||
expect(repo).not_to be_valid
|
expect(repo).not_to be_valid
|
||||||
expect(repo.errors[:platform]).to be_present
|
expect(repo.errors[:platform]).to be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.each do |p|
|
WarpEngine::PlatformLink::SUPPORTED_PLATFORMS.each do |p|
|
||||||
it "accepts #{p}" do
|
it "accepts #{p}" do
|
||||||
repo = build(:ci_repository, platform: p)
|
repo = build(:pipeline, platform: p)
|
||||||
expect(repo).to be_valid
|
expect(repo).to be_valid
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "accepts the unknown placeholder platform" do
|
||||||
|
repo = build(:pipeline, platform: WarpEngine::Pipeline::UNKNOWN_PLATFORM)
|
||||||
|
expect(repo).to be_valid
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#full_name" do
|
describe "#full_name" do
|
||||||
it "returns owner/name" do
|
it "returns owner/name" do
|
||||||
repo = build(:ci_repository, repo_owner: "games", repo_name: "mygame")
|
repo = build(:pipeline, repo_owner: "games", repo_name: "mygame")
|
||||||
expect(repo.full_name).to eq("games/mygame")
|
expect(repo.full_name).to eq("games/mygame")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "default_scope" do
|
describe "default_scope" do
|
||||||
it "excludes soft-deleted records" do
|
it "excludes soft-deleted records" do
|
||||||
repo = create(:ci_repository)
|
repo = create(:pipeline)
|
||||||
repo.update_column(:deleted_at, Time.current)
|
repo.update_column(:deleted_at, Time.current)
|
||||||
|
|
||||||
expect(described_class.all).not_to include(repo)
|
expect(described_class.all).not_to include(repo)
|
||||||
@@ -43,8 +48,8 @@ RSpec.describe WarpEngine::CiRepository do
|
|||||||
|
|
||||||
describe ".active" do
|
describe ".active" do
|
||||||
it "returns only active repos" do
|
it "returns only active repos" do
|
||||||
active = create(:ci_repository, active: true)
|
active = create(:pipeline, active: true)
|
||||||
inactive = create(:ci_repository, active: false)
|
inactive = create(:pipeline, active: false)
|
||||||
|
|
||||||
expect(described_class.active).to include(active)
|
expect(described_class.active).to include(active)
|
||||||
expect(described_class.active).not_to include(inactive)
|
expect(described_class.active).not_to include(inactive)
|
||||||
@@ -7,11 +7,11 @@ RSpec.describe "CI API", type: :request do
|
|||||||
allow(WarpEngine.config).to receive(:update_secret).and_return("s3cret")
|
allow(WarpEngine.config).to receive(:update_secret).and_return("s3cret")
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /api/ci/repos" do
|
describe "GET /api/ci/pipelines" do
|
||||||
it "returns active repos" do
|
it "returns active repos" do
|
||||||
repo = create(:ci_repository, repo_name: "mygame", platform: "tic80")
|
repo = create(:pipeline, repo_name: "mygame", platform: "tic80")
|
||||||
|
|
||||||
get "/api/ci/repos"
|
get "/api/ci/pipelines"
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
json = JSON.parse(response.body)
|
json = JSON.parse(response.body)
|
||||||
@@ -22,45 +22,45 @@ RSpec.describe "CI API", type: :request do
|
|||||||
it "returns 503 when woodpecker not configured" do
|
it "returns 503 when woodpecker not configured" do
|
||||||
allow(WarpEngine.config).to receive(:woodpecker_url).and_return(nil)
|
allow(WarpEngine.config).to receive(:woodpecker_url).and_return(nil)
|
||||||
|
|
||||||
get "/api/ci/repos"
|
get "/api/ci/pipelines"
|
||||||
|
|
||||||
expect(response).to have_http_status(:service_unavailable)
|
expect(response).to have_http_status(:service_unavailable)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /api/ci/repos/:id/status" do
|
describe "GET /api/ci/pipelines/:id/status" do
|
||||||
it "returns repo with pipeline status" do
|
it "returns repo with pipeline status" do
|
||||||
repo = create(:ci_repository, repo_owner: "org", repo_name: "game")
|
repo = create(:pipeline, repo_owner: "org", repo_name: "game")
|
||||||
client = instance_double(WarpEngine::WoodpeckerClient)
|
client = instance_double(WarpEngine::WoodpeckerClient)
|
||||||
allow(WarpEngine::WoodpeckerClient).to receive(:new).and_return(client)
|
allow(WarpEngine::WoodpeckerClient).to receive(:new).and_return(client)
|
||||||
allow(client).to receive(:get_pipeline)
|
allow(client).to receive(:get_pipeline)
|
||||||
.and_return({ "number" => 1, "status" => "success" })
|
.and_return({ "number" => 1, "status" => "success" })
|
||||||
|
|
||||||
get "/api/ci/repos/#{repo.id}/status"
|
get "/api/ci/pipelines/#{repo.id}/status"
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
json = JSON.parse(response.body)
|
json = JSON.parse(response.body)
|
||||||
expect(json["repo"]["repo_name"]).to eq("game")
|
expect(json["pipeline"]["repo_name"]).to eq("game")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "POST /api/ci/repos/:id/trigger" do
|
describe "POST /api/ci/pipelines/:id/trigger" do
|
||||||
it "requires authentication" do
|
it "requires authentication" do
|
||||||
repo = create(:ci_repository)
|
repo = create(:pipeline)
|
||||||
|
|
||||||
post "/api/ci/repos/#{repo.id}/trigger"
|
post "/api/ci/pipelines/#{repo.id}/trigger"
|
||||||
|
|
||||||
expect(response).to have_http_status(:unauthorized)
|
expect(response).to have_http_status(:unauthorized)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "triggers a pipeline with valid secret" do
|
it "triggers a pipeline with valid secret" do
|
||||||
repo = create(:ci_repository, repo_owner: "org", repo_name: "game")
|
repo = create(:pipeline, repo_owner: "org", repo_name: "game")
|
||||||
client = instance_double(WarpEngine::WoodpeckerClient)
|
client = instance_double(WarpEngine::WoodpeckerClient)
|
||||||
allow(WarpEngine::WoodpeckerClient).to receive(:new).and_return(client)
|
allow(WarpEngine::WoodpeckerClient).to receive(:new).and_return(client)
|
||||||
allow(client).to receive(:trigger_pipeline)
|
allow(client).to receive(:trigger_pipeline)
|
||||||
.and_return({ "number" => 7, "status" => "pending" })
|
.and_return({ "number" => 7, "status" => "pending" })
|
||||||
|
|
||||||
post "/api/ci/repos/#{repo.id}/trigger",
|
post "/api/ci/pipelines/#{repo.id}/trigger",
|
||||||
headers: { "X-Update-Secret" => "s3cret" }
|
headers: { "X-Update-Secret" => "s3cret" }
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe WarpEngine::CiPipelineService do
|
RSpec.describe WarpEngine::PipelineService do
|
||||||
let(:client) { instance_double(WarpEngine::WoodpeckerClient) }
|
let(:client) { instance_double(WarpEngine::WoodpeckerClient) }
|
||||||
let(:service) { described_class.new(client: client) }
|
let(:service) { described_class.new(client: client) }
|
||||||
|
|
||||||
describe "#trigger" do
|
describe "#trigger" do
|
||||||
it "delegates to client" do
|
it "delegates to client" do
|
||||||
repo = build(:ci_repository, repo_owner: "org", repo_name: "game")
|
repo = build(:pipeline, repo_owner: "org", repo_name: "game")
|
||||||
allow(client).to receive(:trigger_pipeline).and_return({ "number" => 6 })
|
allow(client).to receive(:trigger_pipeline).and_return({ "number" => 6 })
|
||||||
|
|
||||||
result = service.trigger(repo, branch: "main")
|
result = service.trigger(repo, branch: "main")
|
||||||
@@ -18,7 +18,7 @@ RSpec.describe WarpEngine::CiPipelineService do
|
|||||||
|
|
||||||
describe "#list_pipelines" do
|
describe "#list_pipelines" do
|
||||||
it "returns paginated pipelines and refreshes the repo's cached last pipeline" do
|
it "returns paginated pipelines and refreshes the repo's cached last pipeline" do
|
||||||
repo = create(:ci_repository, repo_owner: "org", repo_name: "game")
|
repo = create(:pipeline, repo_owner: "org", repo_name: "game")
|
||||||
pipelines = [
|
pipelines = [
|
||||||
{ "number" => 2, "status" => "failure", "created" => 1_754_500_000 },
|
{ "number" => 2, "status" => "failure", "created" => 1_754_500_000 },
|
||||||
{ "number" => 1, "status" => "success", "created" => 1_754_400_000 }
|
{ "number" => 1, "status" => "success", "created" => 1_754_400_000 }
|
||||||
@@ -33,7 +33,7 @@ RSpec.describe WarpEngine::CiPipelineService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "does not touch the cache on later pages" do
|
it "does not touch the cache on later pages" do
|
||||||
repo = create(:ci_repository, repo_owner: "org", repo_name: "game",
|
repo = create(:pipeline, repo_owner: "org", repo_name: "game",
|
||||||
last_pipeline_status: "success")
|
last_pipeline_status: "success")
|
||||||
allow(client).to receive(:list_pipelines).with(repo.woodpecker_repo_id, page: 2)
|
allow(client).to receive(:list_pipelines).with(repo.woodpecker_repo_id, page: 2)
|
||||||
.and_return([{ "number" => 1, "status" => "failure", "created" => 1_754_400_000 }])
|
.and_return([{ "number" => 1, "status" => "failure", "created" => 1_754_400_000 }])
|
||||||
@@ -44,7 +44,7 @@ RSpec.describe WarpEngine::CiPipelineService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "leaves the cache alone when the repo has no pipelines" do
|
it "leaves the cache alone when the repo has no pipelines" do
|
||||||
repo = create(:ci_repository, repo_owner: "org", repo_name: "game",
|
repo = create(:pipeline, repo_owner: "org", repo_name: "game",
|
||||||
last_pipeline_status: "success")
|
last_pipeline_status: "success")
|
||||||
allow(client).to receive(:list_pipelines).and_return([])
|
allow(client).to receive(:list_pipelines).and_return([])
|
||||||
|
|
||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe WarpEngine::CiRepoSyncService do
|
RSpec.describe WarpEngine::PipelineSyncService do
|
||||||
let(:client) { instance_double(WarpEngine::WoodpeckerClient) }
|
let(:client) { instance_double(WarpEngine::WoodpeckerClient) }
|
||||||
let(:service) { described_class.new(client: client) }
|
let(:service) { described_class.new(client: client) }
|
||||||
|
|
||||||
describe "#sync_all" do
|
describe "#sync_all" do
|
||||||
it "creates new CiRepository records from Woodpecker" do
|
it "creates new Pipeline records from Woodpecker" do
|
||||||
allow(client).to receive(:list_repos).and_return([
|
allow(client).to receive(:list_repos).and_return([
|
||||||
{ "id" => 1, "name" => "mygame", "owner" => "org", "active" => true }
|
{ "id" => 1, "name" => "mygame", "owner" => "org", "active" => true }
|
||||||
])
|
])
|
||||||
@@ -20,7 +20,7 @@ RSpec.describe WarpEngine::CiRepoSyncService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "updates existing records" do
|
it "updates existing records" do
|
||||||
existing = create(:ci_repository, woodpecker_repo_id: 1, repo_name: "old", platform: "tic80")
|
existing = create(:pipeline, woodpecker_repo_id: 1, repo_name: "old", platform: "tic80")
|
||||||
allow(client).to receive(:list_repos).and_return([
|
allow(client).to receive(:list_repos).and_return([
|
||||||
{ "id" => 1, "name" => "newname", "owner" => "org", "active" => true }
|
{ "id" => 1, "name" => "newname", "owner" => "org", "active" => true }
|
||||||
])
|
])
|
||||||
@@ -32,7 +32,7 @@ RSpec.describe WarpEngine::CiRepoSyncService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "deactivates repos missing from Woodpecker" do
|
it "deactivates repos missing from Woodpecker" do
|
||||||
orphan = create(:ci_repository, woodpecker_repo_id: 99, active: true)
|
orphan = create(:pipeline, woodpecker_repo_id: 99, active: true)
|
||||||
allow(client).to receive(:list_repos).and_return([])
|
allow(client).to receive(:list_repos).and_return([])
|
||||||
|
|
||||||
result = service.sync_all
|
result = service.sync_all
|
||||||
@@ -70,7 +70,7 @@ RSpec.describe WarpEngine::CiRepoSyncService do
|
|||||||
|
|
||||||
describe "#deactivate" do
|
describe "#deactivate" do
|
||||||
it "calls client and marks repo inactive" do
|
it "calls client and marks repo inactive" do
|
||||||
repo = create(:ci_repository, woodpecker_repo_id: 42, active: true)
|
repo = create(:pipeline, woodpecker_repo_id: 42, active: true)
|
||||||
allow(client).to receive(:deactivate_repo).with(42)
|
allow(client).to receive(:deactivate_repo).with(42)
|
||||||
|
|
||||||
service.deactivate(42)
|
service.deactivate(42)
|
||||||
+18
-18
@@ -1,6 +1,6 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe WarpEngine::CiSecretSyncService do
|
RSpec.describe WarpEngine::SecretSyncService do
|
||||||
let(:client) { instance_double(WarpEngine::WoodpeckerClient) }
|
let(:client) { instance_double(WarpEngine::WoodpeckerClient) }
|
||||||
let(:service) { described_class.new(client: client) }
|
let(:service) { described_class.new(client: client) }
|
||||||
|
|
||||||
@@ -11,11 +11,11 @@ RSpec.describe WarpEngine::CiSecretSyncService do
|
|||||||
|
|
||||||
describe "#provision" do
|
describe "#provision" do
|
||||||
it "creates secrets on repos that don't have one" do
|
it "creates secrets on repos that don't have one" do
|
||||||
repo = create(:ci_repository)
|
repo = create(:pipeline)
|
||||||
allow(client).to receive(:list_secrets).with(repo.woodpecker_repo_id).and_return([])
|
allow(client).to receive(:list_secrets).with(repo.woodpecker_repo_id).and_return([])
|
||||||
allow(client).to receive(:create_secret)
|
allow(client).to receive(:create_secret)
|
||||||
|
|
||||||
result = service.provision("plaintoken", repos: [ repo ])
|
result = service.provision("plaintoken", pipelines: [ repo ])
|
||||||
|
|
||||||
expect(result[:synced]).to eq([ repo ])
|
expect(result[:synced]).to eq([ repo ])
|
||||||
expect(client).to have_received(:create_secret).with(
|
expect(client).to have_received(:create_secret).with(
|
||||||
@@ -24,12 +24,12 @@ RSpec.describe WarpEngine::CiSecretSyncService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "updates secrets on repos that already have one" do
|
it "updates secrets on repos that already have one" do
|
||||||
repo = create(:ci_repository)
|
repo = create(:pipeline)
|
||||||
allow(client).to receive(:list_secrets).with(repo.woodpecker_repo_id)
|
allow(client).to receive(:list_secrets).with(repo.woodpecker_repo_id)
|
||||||
.and_return([ { "name" => "application_token" } ])
|
.and_return([ { "name" => "application_token" } ])
|
||||||
allow(client).to receive(:update_secret)
|
allow(client).to receive(:update_secret)
|
||||||
|
|
||||||
result = service.provision("newtoken", repos: [ repo ])
|
result = service.provision("newtoken", pipelines: [ repo ])
|
||||||
|
|
||||||
expect(result[:synced]).to eq([ repo ])
|
expect(result[:synced]).to eq([ repo ])
|
||||||
expect(client).to have_received(:update_secret).with(
|
expect(client).to have_received(:update_secret).with(
|
||||||
@@ -38,12 +38,12 @@ RSpec.describe WarpEngine::CiSecretSyncService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "records failed repos without raising" do
|
it "records failed repos without raising" do
|
||||||
repo = create(:ci_repository)
|
repo = create(:pipeline)
|
||||||
allow(client).to receive(:list_secrets).and_raise(
|
allow(client).to receive(:list_secrets).and_raise(
|
||||||
WarpEngine::WoodpeckerClient::ConnectionError, "unreachable"
|
WarpEngine::WoodpeckerClient::ConnectionError, "unreachable"
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service.provision("tok", repos: [ repo ])
|
result = service.provision("tok", pipelines: [ repo ])
|
||||||
|
|
||||||
expect(result[:synced]).to be_empty
|
expect(result[:synced]).to be_empty
|
||||||
expect(result[:failed].size).to eq(1)
|
expect(result[:failed].size).to eq(1)
|
||||||
@@ -54,7 +54,7 @@ RSpec.describe WarpEngine::CiSecretSyncService do
|
|||||||
it "deletes secrets from all relevant repos" do
|
it "deletes secrets from all relevant repos" do
|
||||||
token = create(:application_token)
|
token = create(:application_token)
|
||||||
sw = create(:software, name: "game1", owner: token.owner)
|
sw = create(:software, name: "game1", owner: token.owner)
|
||||||
repo = create(:ci_repository, :with_software, software: sw)
|
repo = create(:pipeline, :with_software, software: sw)
|
||||||
|
|
||||||
allow(client).to receive(:delete_secret)
|
allow(client).to receive(:delete_secret)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ RSpec.describe WarpEngine::CiSecretSyncService do
|
|||||||
it "creates new token, provisions, revokes old" do
|
it "creates new token, provisions, revokes old" do
|
||||||
token = create(:application_token)
|
token = create(:application_token)
|
||||||
sw = create(:software, name: "game1", owner: token.owner)
|
sw = create(:software, name: "game1", owner: token.owner)
|
||||||
repo = create(:ci_repository, :with_software, software: sw)
|
repo = create(:pipeline, :with_software, software: sw)
|
||||||
|
|
||||||
allow(client).to receive(:list_secrets).and_return([])
|
allow(client).to receive(:list_secrets).and_return([])
|
||||||
allow(client).to receive(:create_secret)
|
allow(client).to receive(:create_secret)
|
||||||
@@ -83,7 +83,7 @@ RSpec.describe WarpEngine::CiSecretSyncService do
|
|||||||
it "rolls back if all repos fail" do
|
it "rolls back if all repos fail" do
|
||||||
token = create(:application_token)
|
token = create(:application_token)
|
||||||
sw = create(:software, name: "game1", owner: token.owner)
|
sw = create(:software, name: "game1", owner: token.owner)
|
||||||
create(:ci_repository, :with_software, software: sw)
|
create(:pipeline, :with_software, software: sw)
|
||||||
|
|
||||||
allow(client).to receive(:list_secrets).and_raise(
|
allow(client).to receive(:list_secrets).and_raise(
|
||||||
WarpEngine::WoodpeckerClient::ConnectionError, "down"
|
WarpEngine::WoodpeckerClient::ConnectionError, "down"
|
||||||
@@ -101,17 +101,17 @@ RSpec.describe WarpEngine::CiSecretSyncService do
|
|||||||
result = service.rotate(token)
|
result = service.rotate(token)
|
||||||
|
|
||||||
expect(result[:rotated]).to be false
|
expect(result[:rotated]).to be false
|
||||||
expect(result[:reason]).to eq("no repos")
|
expect(result[:reason]).to eq("no pipelines")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#repos_for_token" do
|
describe "#pipelines_for_token" do
|
||||||
it "returns all active repos for unrestricted tokens" do
|
it "returns all active repos for unrestricted tokens" do
|
||||||
token = create(:application_token, :unrestricted)
|
token = create(:application_token, :unrestricted)
|
||||||
repo1 = create(:ci_repository)
|
repo1 = create(:pipeline)
|
||||||
create(:ci_repository, :inactive)
|
create(:pipeline, :inactive)
|
||||||
|
|
||||||
repos = service.repos_for_token(token)
|
repos = service.pipelines_for_token(token)
|
||||||
|
|
||||||
expect(repos).to eq([ repo1 ])
|
expect(repos).to eq([ repo1 ])
|
||||||
end
|
end
|
||||||
@@ -120,10 +120,10 @@ RSpec.describe WarpEngine::CiSecretSyncService do
|
|||||||
token = create(:application_token)
|
token = create(:application_token)
|
||||||
own_sw = create(:software, name: "mine", owner: token.owner)
|
own_sw = create(:software, name: "mine", owner: token.owner)
|
||||||
other_sw = create(:software, name: "theirs", owner: create(:test_owner))
|
other_sw = create(:software, name: "theirs", owner: create(:test_owner))
|
||||||
own_repo = create(:ci_repository, :with_software, software: own_sw)
|
own_repo = create(:pipeline, :with_software, software: own_sw)
|
||||||
create(:ci_repository, :with_software, software: other_sw)
|
create(:pipeline, :with_software, software: other_sw)
|
||||||
|
|
||||||
repos = service.repos_for_token(token)
|
repos = service.pipelines_for_token(token)
|
||||||
|
|
||||||
expect(repos).to eq([ own_repo ])
|
expect(repos).to eq([ own_repo ])
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user