Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

module Api::V1::ReleaseEngines
# Squirrel.Windows RELEASES endpoint. Redirects to the RELEASES artifact
# uploaded by the user for the latest available upgrade. The RELEASES
# file is a build artifact generated by electron-builder or Squirrel
# that contains the nupkg manifest.
#
# See: https://github.com/electron/update.electronjs.org
class Electron::ReleasesController < Api::V1::BaseController
before_action :scope_to_current_account!
before_action :require_active_subscription!
before_action :authenticate_with_token
before_action :set_package, only: %i[show]

typed_query {
param :constraint, type: :string, optional: true
param :channel, type: :string, optional: true
}
def show
authorize! package

platform = params[:platform]
arch = params[:arch]

releases = authorized_scope(package.releases)
release = releases.find_by!(version: params[:version])
authorize! release, to: :upgrade?

kwargs = release_query.slice(:constraint, :channel)
upgrade = release.upgrade!(**kwargs)
authorize! upgrade

artifacts = authorized_scope(upgrade.artifacts)
artifact = artifacts.joins(:platform, :arch)
.where(
release_artifacts: { filename: 'RELEASES' },
platform: { key: platform },
arch: { key: arch },
)
.reorder(created_at: :desc)
.first!
authorize! artifact

BroadcastEventService.call(
event: 'release.upgraded',
account: current_account,
resource: upgrade,
meta: {
current: release.version,
next: upgrade.version,
},
)

redirect_to vanity_v1_account_release_artifact_url(artifact.account, artifact, filename: artifact.filename, host: request.host),
status: :see_other,
allow_other_host: true
rescue ActiveRecord::RecordNotFound
render_no_content
end

private

attr_reader :package

def set_package
@package = Current.resource = FindByAliasService.call(
authorized_scope(current_account.release_packages.electron),
id: params[:package],
aliases: :key,
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

module Api::V1::ReleaseEngines
class Electron::UpgradesController < Api::V1::BaseController
before_action :scope_to_current_account!
before_action :require_active_subscription!
before_action :authenticate_with_token
before_action :set_package, only: %i[show]

typed_query {
param :constraint, type: :string, optional: true
param :channel, type: :string, optional: true
}
def show
authorize! package

platform = params[:platform]
arch = params[:arch]

releases = authorized_scope(package.releases)
release = releases.find_by!(version: params[:version])
authorize! release, to: :upgrade?

kwargs = upgrade_query.slice(:constraint, :channel)
upgrade = release.upgrade!(**kwargs)
authorize! upgrade

artifacts = authorized_scope(upgrade.artifacts)
artifact = artifacts.joins(:platform, :arch, :filetype)
.where.not(filetype: { key: %w[sig] })
.reorder(
# NOTE(ezekg) Prioritize artifact formats for Electron:
#
# macOS: .zip takes precedence (required by Squirrel.Mac)
# Windows: .exe for NSIS/Squirrel.Windows, .nupkg for legacy
# MSIX: .appx/.msix files
#
Arel.sql(<<~SQL.squish)
release_artifacts.filename ILIKE '%.zip' DESC,
release_artifacts.filename ILIKE '%.exe' DESC,
release_artifacts.filename ILIKE '%.nupkg' DESC,
release_artifacts.filename ILIKE '%.appx' DESC,
release_artifacts.filename ILIKE '%.msix' DESC,
release_artifacts.created_at DESC
SQL
)
.find_by!(
platform: { key: platform },
arch: { key: arch },
)
authorize! artifact

BroadcastEventService.call(
event: 'release.upgraded',
account: current_account,
resource: upgrade,
meta: {
current: release.version,
next: upgrade.version,
},
)

# See: https://www.electronjs.org/docs/latest/tutorial/updates
render json: {
url: vanity_v1_account_release_artifact_url(artifact.account, artifact, filename: artifact.filename, host: request.host),
name: upgrade.version,
notes: upgrade.description,
pub_date: upgrade.created_at.rfc3339(3),
}
rescue ActiveRecord::RecordNotFound
render_no_content
end

private

attr_reader :package

def set_package
@package = Current.resource = FindByAliasService.call(
authorized_scope(current_account.release_packages.electron),
id: params[:package],
aliases: :key,
)
end
end
end
2 changes: 2 additions & 0 deletions app/models/release_engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ReleaseEngine < ApplicationRecord
rubygems
npm
oci
electron
]

has_many :packages,
Expand Down Expand Up @@ -118,6 +119,7 @@ def raw? = key == 'raw'
def rubygems? = key == 'rubygems'
def npm? = key == 'npm'
def oci? = key == 'oci'
def electron? = key == 'electron'

##
# deconstruct allows pattern pattern matching like:
Expand Down
3 changes: 2 additions & 1 deletion app/models/release_package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ class ReleasePackage < ApplicationRecord
scope :rubygems, -> { for_engine_key('rubygems') }
scope :npm, -> { for_engine_key('npm') }
scope :oci, -> { for_engine_key('oci') }
scope :electron, -> { for_engine_key('electron') }

delegate :pypi?, :tauri?, :raw?, :rubygems?, :npm?, :oci?,
delegate :pypi?, :tauri?, :raw?, :rubygems?, :npm?, :oci?, :electron?,
to: :engine,
allow_nil: true

Expand Down
26 changes: 26 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@
end
end

concern :electron do
# see: https://www.electronjs.org/docs/latest/tutorial/updates
scope module: :electron, constraints: { version: /[^\/]+/ } do
get ':package/:platform-:arch/:version/RELEASES', to: 'releases#show', as: :electron_releases,
constraints: MimeTypeConstraint.new(:text, :binary, raise_on_no_match: true),
defaults: { format: :text }
get ':package/:platform-:arch/:version', to: 'upgrades#show', as: :electron_upgrade_package,
constraints: MimeTypeConstraint.new(:binary, :json, raise_on_no_match: true),
defaults: { format: :json }
end
end

concern :raw do
scope module: :raw, defaults: { format: :binary } do
get ':product(/@:package)/:release/:artifact', to: 'release_artifacts#show', as: :raw_download_artifact, constraints: {
Expand Down Expand Up @@ -455,6 +467,9 @@
scope :tauri do
concerns :tauri
end
scope :electron do
concerns :electron
end
scope :raw do
concerns :raw
end
Expand Down Expand Up @@ -603,6 +618,17 @@
end
end

scope module: 'api/v1/release_engines', constraints: { subdomain: 'electron.pkg' } do
case
when Keygen.multiplayer?
scope ':account_id', as: :account do
concerns :electron
end
when Keygen.singleplayer?
concerns :electron
end
end

scope module: 'api/v1/release_engines', constraints: { subdomain: 'raw.pkg' } do
case
when Keygen.multiplayer?
Expand Down
129 changes: 129 additions & 0 deletions features/api/v1/engines/electron/releases.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
@api/v1
Feature: Electron RELEASES for Squirrel.Windows
Background:
Given the following "accounts" exist:
| name | slug |
| Test 1 | test1 |
| Test 2 | test2 |
And the current account is "test1"
And the current account has the following "product" rows:
| id | name |
| 6198261a-48b5-4445-a045-9fed4afc7735 | Test |
And the current account has the following "package" rows:
| id | product_id | engine | key |
| 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 6198261a-48b5-4445-a045-9fed4afc7735 | electron | app1 |
| 7b113ac2-ae81-406a-b44e-f356126e2faa | 6198261a-48b5-4445-a045-9fed4afc7735 | pypi | pkg1 |
And the current account has the following "release" rows:
| id | product_id | release_package_id | version | channel | description |
| 757e0a41-835e-42ad-bad8-84cabd29c72a | 6198261a-48b5-4445-a045-9fed4afc7735 | 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 1.0.0 | stable | foo |
| 028a38a2-0d17-4871-acb8-c5e6f040fc12 | 6198261a-48b5-4445-a045-9fed4afc7735 | 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 1.1.0 | stable | bar |
And the current account has the following "artifact" rows:
| id | release_id | filename | filetype | platform | arch |
# 1.0.0
| 1f63d6ec-8147-4bf0-bcd2-5d4f0e5eab8f | 757e0a41-835e-42ad-bad8-84cabd29c72a | myapp-1.0.0-darwin-x64.zip | zip | darwin | x64 |
| fa773c2b-1c3a-4bd8-83fe-546480e92098 | 757e0a41-835e-42ad-bad8-84cabd29c72a | myapp-1.0.0-win32-x64-setup.exe | exe | win32 | x64 |
| ab3f9749-3ea7-4057-92ec-d647784ff097 | 757e0a41-835e-42ad-bad8-84cabd29c72a | myapp-1.0.0-full.nupkg | nupkg | win32 | x64 |
| d7e01e53-4f9c-48a5-96cb-13207fc25cfe | 757e0a41-835e-42ad-bad8-84cabd29c72a | RELEASES | | win32 | x64 |
# 1.1.0
| 00aeec65-165c-487c-8e22-7ab454319b0f | 028a38a2-0d17-4871-acb8-c5e6f040fc12 | myapp-1.1.0-darwin-x64.zip | zip | darwin | x64 |
| 2133955c-137f-4422-9290-9a364b1a40a0 | 028a38a2-0d17-4871-acb8-c5e6f040fc12 | myapp-1.1.0-win32-x64-setup.exe | exe | win32 | x64 |
| eaa67d65-f596-427a-8f64-80a7125ae299 | 028a38a2-0d17-4871-acb8-c5e6f040fc12 | myapp-1.1.0-full.nupkg | nupkg | win32 | x64 |
| c185d92b-1232-4bdd-9906-fa4d99e259c7 | 028a38a2-0d17-4871-acb8-c5e6f040fc12 | RELEASES | | win32 | x64 |
And I send the following raw headers:
"""
Accept: application/octet-stream
"""

Scenario: Endpoint should be inaccessible when account is disabled
Given the account "test1" is canceled
And I am an admin of account "test1"
And I use an authentication token
When I send a GET request to "/accounts/test1/engines/electron/app1/win32-x64/1.0.0/RELEASES"
Then the response status should be "403"

Scenario: Endpoint should redirect to RELEASES artifact when an upgrade is available
Given the current account has 1 "webhook-endpoint"
And I am an admin of account "test1"
And I use an authentication token
When I send a GET request to "/accounts/test1/engines/electron/app1/win32-x64/1.0.0/RELEASES"
Then the response status should be "303"
And the response should contain the following headers:
"""
{
"Location": "https://api.keygen.sh/v1/accounts/$account/artifacts/c185d92b-1232-4bdd-9906-fa4d99e259c7/RELEASES"
}
"""
And sidekiq should have 1 "webhook" job
And sidekiq should have 1 "event-log" job
And sidekiq should have 1 "request-log" job

Scenario: Endpoint should not return RELEASES when no upgrade is available
Given the current account has 1 "webhook-endpoint"
And I am an admin of account "test1"
And I use an authentication token
When I send a GET request to "/accounts/test1/engines/electron/app1/win32-x64/1.1.0/RELEASES"
Then the response status should be "204"
And sidekiq should have 0 "webhook" jobs
And sidekiq should have 0 "event-log" jobs
And sidekiq should have 1 "request-log" job

Scenario: Endpoint should return error for non-Electron packages
Given I am an admin of account "test1"
And I use an authentication token
When I send a GET request to "/accounts/test1/engines/electron/pkg1/win32-x64/1.0.0/RELEASES"
Then the response status should be "404"

@mp
Scenario: Endpoint should be accessible from subdomain
Given the current account has 1 "webhook-endpoint"
And I am an admin of account "test1"
And I use an authentication token
When I send a GET request to "//electron.pkg.keygen.sh/test1/app1/win32-x64/1.0.0/RELEASES"
Then the response status should be "303"
And the response should contain the following headers:
"""
{
"Location": "https://electron.pkg.keygen.sh/v1/accounts/$account/artifacts/c185d92b-1232-4bdd-9906-fa4d99e259c7/RELEASES"
}
"""

@sp
Scenario: Endpoint should be accessible from subdomain
Given the current account has 1 "webhook-endpoint"
And I am an admin of account "test1"
And I use an authentication token
When I send a GET request to "//electron.pkg.keygen.sh/app1/win32-x64/1.0.0/RELEASES"
Then the response status should be "303"
And the response should contain the following headers:
"""
{
"Location": "https://electron.pkg.keygen.sh/v1/accounts/$account/artifacts/c185d92b-1232-4bdd-9906-fa4d99e259c7/RELEASES"
}
"""

Scenario: License retrieves RELEASES when an upgrade is available
Given the current account has 1 "policy" for the last "product" with the following:
"""
{ "authenticationStrategy": "LICENSE" }
"""
And the current account has 1 "license" for the last "policy"
And I am a license of account "test1"
And I authenticate with my key
When I send a GET request to "/accounts/test1/engines/electron/app1/win32-x64/1.0.0/RELEASES"
Then the response status should be "303"

Scenario: Anonymous retrieves RELEASES for a licensed product
Given the last "product" has the following attributes:
"""
{ "distributionStrategy": "LICENSED" }
"""
When I send a GET request to "/accounts/test1/engines/electron/app1/win32-x64/1.0.0/RELEASES"
Then the response status should be "404"

Scenario: Anonymous retrieves RELEASES for an open product
Given the last "product" has the following attributes:
"""
{ "distributionStrategy": "OPEN" }
"""
When I send a GET request to "/accounts/test1/engines/electron/app1/win32-x64/1.0.0/RELEASES"
Then the response status should be "303"
Loading