# frozen_string_literal: true module API class Releases < ::API::Base include PaginationParams releases_tags = %w[releases] RELEASE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS .merge(tag_name: API::NO_SLASH_URL_PART_REGEX) RELEASE_CLI_USER_AGENT = 'GitLab-release-cli' feature_category :release_orchestration urgency :low params do requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the group' end resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before { authorize_read_group_releases! } desc 'List group releases' do detail 'Returns a list of group releases.' success Entities::Release failure [ { code: 400, message: 'Bad request' }, { code: 403, message: 'Forbidden' }, { code: 404, message: 'Not found' } ] is_array true tags releases_tags end params do requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the group owned by the authenticated user' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'The direction of the order. Either `desc` (default) for descending order or `asc` for ascending order' optional :simple, type: Boolean, default: false, desc: 'Return only limited fields for each release' use :pagination end get ":id/releases" do finder_options = { sort: params[:sort] } strict_params = declared_params(include_missing: false) releases = find_group_releases(finder_options) present_group_releases(strict_params, releases) end end params do requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before { authorize_read_releases! } after { track_release_event } desc 'List Releases' do detail 'Returns a paginated list of releases. This feature was introduced in GitLab 11.7.' named 'get_releases' is_array true success Entities::Release tags releases_tags end params do use :pagination optional :order_by, type: String, values: %w[released_at created_at], default: 'released_at', desc: 'The field to use as order. Either `released_at` (default) or `created_at`' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'The direction of the order. Either `desc` (default) for descending order or `asc` for ascending order' optional :include_html_description, type: Boolean, desc: 'If `true`, a response includes HTML rendered markdown of the release description' end route_setting :authentication, job_token_allowed: true get ':id/releases' do releases = ::ReleasesFinder.new(user_project, current_user, declared_params.slice(:order_by, :sort)).execute # We cache the serialized payload per user in order to avoid repeated renderings. # Since the cached result could contain sensitive information, # it will expire in a short interval. present_cached paginate(releases), with: Entities::Release, # `current_user` could be absent if the releases are publicly accesible. # We should not use `cache_key` for the user because the version/updated_at # context is unnecessary here. cache_context: -> (_) { "user:{#{current_user&.id}}" }, expires_in: 5.minutes, current_user: current_user, include_html_description: params[:include_html_description] end desc 'Get a release by a tag name' do detail 'Gets a release for the given tag. This feature was introduced in GitLab 11.7.' named 'get_release' success Entities::Release failure [ { code: 401, message: 'Unauthorized' }, { code: 404, message: 'Not found' } ] tags releases_tags end params do requires :tag_name, type: String, desc: 'The Git tag the release is associated with', as: :tag optional :include_html_description, type: Boolean, desc: 'If `true`, a response includes HTML rendered markdown of the release description' end route_setting :authentication, job_token_allowed: true get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do authorize_download_code! not_found! unless release present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description] end desc 'Download a project release asset file' do detail 'This feature was introduced in GitLab 15.4.' named 'download_release_asset_file' failure [ { code: 401, message: 'Unauthorized' }, { code: 404, message: 'Not found' } ] tags releases_tags end params do requires :tag_name, type: String, desc: 'The Git tag the release is associated with', as: :tag requires :file_path, type: String, file_path: true, desc: 'The path to the file to download, as specified when creating the release asset' end route_setting :authentication, job_token_allowed: true get ':id/releases/:tag_name/downloads/*file_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do authorize_download_code! not_found! unless release link = release.links.find_by_filepath!("/#{params[:file_path]}") not_found! unless link redirect link.url end desc 'Get the latest project release' do detail 'This feature was introduced in GitLab 15.4.' named 'get_latest_release' failure [ { code: 401, message: 'Unauthorized' }, { code: 404, message: 'Not found' } ] tags releases_tags end params do requires :suffix_path, type: String, file_path: true, desc: 'The path to be suffixed to the latest release' end route_setting :authentication, job_token_allowed: true get ':id/releases/permalink/latest(/)(*suffix_path)', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do authorize_download_code! # Try to find the latest release latest_release = find_latest_release not_found! unless latest_release # Build the full API URL with the tag of the latest release redirect_url = api_v4_projects_releases_path(id: user_project.id, tag_name: latest_release.tag) # Include the additional suffix_path if present redirect_url += "/#{params[:suffix_path]}" if params[:suffix_path].present? # Include any query parameter except `order_by` since we have plans to extend it in the future. # See https://gitlab.com/gitlab-org/gitlab/-/issues/352945 for reference. query_parameters_except_order_by = get_query_params.except('order_by') if query_parameters_except_order_by.present? redirect_url += "?#{query_parameters_except_order_by.compact.to_param}" end redirect redirect_url end desc 'Create a release' do detail 'Creates a release. Developer level access to the project is required to create a release. This feature was introduced in GitLab 11.7.' named 'create_release' success Entities::Release failure [ { code: 400, message: 'Bad request' }, { code: 401, message: 'Unauthorized' }, { code: 403, message: 'Forbidden' }, { code: 404, message: 'Not found' }, { code: 409, message: 'Conflict' }, { code: 422, message: 'Unprocessable entity' } ] tags releases_tags end params do requires :tag_name, type: String, desc: 'The tag where the release is created from', as: :tag optional :tag_message, type: String, desc: 'Message to use if creating a new annotated tag' optional :name, type: String, desc: 'The release name' optional :description, type: String, desc: 'The description of the release. You can use Markdown' optional :ref, type: String, desc: "If a tag specified in `tag_name` doesn't exist, the release is created from `ref` and tagged " \ "with `tag_name`. It can be a commit SHA, another tag name, or a branch name." optional :assets, type: Hash do optional :links, type: Array do requires :name, type: String, desc: 'The name of the link. Link names must be unique within the release' requires :url, type: String, desc: 'The URL of the link. Link URLs must be unique within the release' optional :filepath, type: String, desc: 'Optional path for a direct asset link' optional :link_type, type: String, desc: 'The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`' end end optional :milestones, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones', default: [] optional :released_at, type: DateTime, desc: 'Date and time for the release. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). ' \ 'Only provide this field if creating an upcoming or historical release.' end route_setting :authentication, job_token_allowed: true post ':id/releases' do authorize_create_release! result = ::Releases::CreateService .new(user_project, current_user, declared_params(include_missing: false)) .execute if result[:status] == :success log_release_created_audit_event(result[:release]) present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end end desc 'Update a release' do detail 'Updates a release. Developer level access to the project is required to update a release. This feature was introduced in GitLab 11.7.' named 'update_release' success Entities::Release failure [ { code: 400, message: 'Bad request' }, { code: 401, message: 'Unauthorized' }, { code: 403, message: 'Forbidden' }, { code: 404, message: 'Not found' } ] tags releases_tags end params do requires :tag_name, type: String, desc: 'The Git tag the release is associated with', as: :tag optional :name, type: String, desc: 'The release name' optional :description, type: String, desc: 'The description of the release. You can use Markdown' optional :released_at, type: DateTime, desc: 'The date when the release is/was ready. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`)' optional :milestones, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The title of each milestone to associate with the release. GitLab Premium customers can specify group milestones. To remove all milestones from the release, specify `[]`' end route_setting :authentication, job_token_allowed: true put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do authorize_update_release! result = ::Releases::UpdateService .new(user_project, current_user, declared_params(include_missing: false)) .execute if result[:status] == :success log_release_updated_audit_event log_release_milestones_updated_audit_event if result[:milestones_updated] present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end end desc 'Delete a release' do detail "Delete a release. Deleting a release doesn't delete the associated tag. Maintainer level access to the project is required to delete a release. This feature was introduced in GitLab 11.7." named 'delete_release' success Entities::Release failure [ { code: 400, message: 'Bad request' }, { code: 401, message: 'Unauthorized' }, { code: 403, message: 'Forbidden' }, { code: 404, message: 'Not found' } ] tags releases_tags end params do requires :tag_name, type: String, desc: 'The Git tag the release is associated with', as: :tag end route_setting :authentication, job_token_allowed: true delete ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do authorize_destroy_release! result = ::Releases::DestroyService .new(user_project, current_user, declared_params(include_missing: false)) .execute if result[:status] == :success log_release_deleted_audit_event present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end end end helpers do def authorize_read_group_releases! authorize! :read_release, user_group end def authorize_create_release! authorize! :create_release, user_project end def authorize_read_releases! authorize! :read_release, user_project end def authorize_read_release! authorize! :read_release, release end def authorize_update_release! authorize! :update_release, release end def authorize_destroy_release! authorize! :destroy_release, release end def authorize_download_code! authorize! :download_code, user_project end def authorize_create_evidence! # extended in EE end def release @release ||= user_project.releases.find_by_tag(params[:tag]) end def find_latest_release ReleasesFinder.new(user_project, current_user, { order_by: 'released_at', sort: 'desc' }).execute.first end def get_query_params return {} unless @request.query_string.present? Rack::Utils.parse_nested_query(@request.query_string) end def log_release_created_audit_event(release) # extended in EE end def log_release_updated_audit_event # extended in EE end def log_release_deleted_audit_event # extended in EE end def log_release_milestones_updated_audit_event # extended in EE end def release_cli? request.env['HTTP_USER_AGENT']&.include?(RELEASE_CLI_USER_AGENT) == true end def event_context { release_cli: release_cli? } end def track_release_event Gitlab::Tracking.event(options[:for].name, options[:route_options][:named], project: user_project, user: current_user, **event_context) end def find_group_releases(finder_options) ::Releases::GroupReleasesFinder .new(user_group, current_user, finder_options) .execute(preload: true) end def present_group_releases(params, releases) options = { with: params[:simple] ? Entities::BasicReleaseDetails : Entities::Release, current_user: current_user } # GroupReleasesFinder has already ordered the data for us present paginate(releases, skip_default_order: true), options end end end end API::Releases.prepend_mod_with('API::Releases')