Skip to content
Open
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
66 changes: 57 additions & 9 deletions app/models/oidc/trusted_publisher/github_action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,39 @@ class OIDC::TrustedPublisher::GitHubAction < ApplicationRecord
validates :repository_owner, :repository_name, :workflow_filename, :repository_owner_id,
presence: true, length: { maximum: Gemcutter::MAX_FIELD_LENGTH }
validates :environment, allow_nil: true, length: { maximum: Gemcutter::MAX_FIELD_LENGTH }
validates :workflow_repository_owner, :workflow_repository_name,
allow_nil: true, length: { maximum: Gemcutter::MAX_FIELD_LENGTH }

validate :unique_publisher
validate :workflow_filename_format
validate :workflow_repository_fields_consistency
validate :workflow_repository_differs_from_repository

def self.for_claims(claims)
repository = claims.fetch(:repository)
repository_owner, repository_name = repository.split("/", 2)
workflow_prefix = "#{repository}/.github/workflows/"
workflow_ref = claims.fetch(:job_workflow_ref).delete_prefix(workflow_prefix)
workflow_filename = workflow_ref.sub(/@[^@]+\z/, "")
job_workflow_ref = claims.fetch(:job_workflow_ref)

match = job_workflow_ref.match(%r{\A([^/]+)/([^/]+)/\.github/workflows/([^@]+)@})
raise ActiveRecord::RecordNotFound, "Invalid job_workflow_ref format" unless match

workflow_repo_owner, workflow_repo_name, workflow_filename = match.captures

required = {
repository_owner:, repository_name:, workflow_filename:,
repository_owner_id: claims.fetch(:repository_owner_id)
}

base = where(required)

same_repo = workflow_repo_owner == repository_owner && workflow_repo_name == repository_name
base = if same_repo
base.where(workflow_repository_owner: workflow_repo_owner, workflow_repository_name: workflow_repo_name)
.or(base.where(workflow_repository_owner: nil, workflow_repository_name: nil))
else
base.where(workflow_repository_owner: workflow_repo_owner, workflow_repository_name: workflow_repo_name)
end
Comment on lines +39 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering that after this is shipped, maybe instead of the columns being optional, we make it required and backfill values to match the org and repo names. That way here, instead of also checking for nil values, we can solely query for the workflow_repo_ownerand workflow_repository_name.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be great. All the for_claims stuff would become more simple. Also simpler queries - No .or() clause needed, single path through for_claims, simpler validation, just remove workflow_repository_fields_consistency and workflow_repository_differs_from_repository entirely. And you would have more explicit data, every record clearly states where its workflow lives, no implicit "nil means same repo". I am definitely for that.


if (env = claims[:environment])
base.where(environment: env).or(base.where(environment: nil)).order(environment: :asc) # NULLS LAST by default for asc
else
Expand All @@ -36,13 +52,17 @@ def self.for_claims(claims)
end

def self.permitted_attributes
%i[repository_owner repository_name workflow_filename environment]
%i[repository_owner repository_name workflow_filename environment
workflow_repository_owner workflow_repository_name]
end

def self.build_trusted_publisher(params)
params = params.reverse_merge(repository_owner_id: nil, repository_name: nil, workflow_filename: nil, environment: nil)
params = params.reverse_merge(repository_owner_id: nil, repository_name: nil, workflow_filename: nil, environment: nil,
workflow_repository_owner: nil, workflow_repository_name: nil)
params.delete(:repository_owner_id)
params[:environment] = nil if params[:environment].blank?
params[:workflow_repository_owner] = nil if params[:workflow_repository_owner].blank?
params[:workflow_repository_name] = nil if params[:workflow_repository_name].blank?
find_or_initialize_by(params)
end

Expand All @@ -55,7 +75,9 @@ def payload
repository_name:,
repository_owner_id:,
workflow_filename:,
environment:
environment:,
workflow_repository_owner:,
workflow_repository_name:
}
end

Expand Down Expand Up @@ -98,7 +120,7 @@ def job_workflow_ref_condition(ref)
OIDC::AccessPolicy::Statement::Condition.new(
operator: "string_equals",
claim: "job_workflow_ref",
value: "#{repository}/#{workflow_slug}@#{ref}"
value: "#{workflow_repository}/#{workflow_slug}@#{ref}"
)
end

Expand Down Expand Up @@ -127,7 +149,7 @@ def initialize(trusted_publisher)
def verify(cert)
ref = cert.openssl.find_extension("1.3.6.1.4.1.57264.1.14")&.value_der&.then { OpenSSL::ASN1.decode(it).value }
Sigstore::Policy::Identity.new(
identity: "https://github.com/#{@trusted_publisher.repository}/#{@trusted_publisher.workflow_slug}@#{ref}",
identity: "https://github.com/#{@trusted_publisher.workflow_repository}/#{@trusted_publisher.workflow_slug}@#{ref}",
issuer: OIDC::Provider::GITHUB_ACTIONS_ISSUER
).verify(cert)
end
Expand All @@ -145,6 +167,14 @@ def name

def repository = "#{repository_owner}/#{repository_name}"

def workflow_repository
if workflow_repository_owner.present? && workflow_repository_name.present?
"#{workflow_repository_owner}/#{workflow_repository_name}"
else
repository
end
end

def workflow_slug = ".github/workflows/#{workflow_filename}"

def owns_gem?(rubygem) = rubygem_trusted_publishers.exists?(rubygem: rubygem)
Expand All @@ -169,7 +199,9 @@ def unique_publisher
repository_name: repository_name,
repository_owner_id: repository_owner_id,
workflow_filename: workflow_filename,
environment: environment
environment: environment,
workflow_repository_owner: workflow_repository_owner,
workflow_repository_name: workflow_repository_name
)

errors.add(:base, "publisher already exists")
Expand All @@ -181,4 +213,20 @@ def workflow_filename_format
errors.add(:workflow_filename, "must end with .yml or .yaml") unless /\.ya?ml\z/.match?(workflow_filename)
errors.add(:workflow_filename, "must be a filename only, without directories") if workflow_filename.include?("/")
end

def workflow_repository_fields_consistency
owner_present = workflow_repository_owner.present?
name_present = workflow_repository_name.present?

return if owner_present == name_present

errors.add(:base, "workflow_repository_owner and workflow_repository_name must both be set or both be blank")
end

def workflow_repository_differs_from_repository
return if workflow_repository_owner.blank? && workflow_repository_name.blank?
return unless workflow_repository_owner == repository_owner && workflow_repository_name == repository_name

errors.add(:base, "workflow_repository must be different from the repository, leave blank for same-repository workflows")
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ def view_template
field trusted_publisher_form, :text_field, :repository_name, autocomplete: :off
field trusted_publisher_form, :text_field, :workflow_filename, autocomplete: :off
field trusted_publisher_form, :text_field, :environment, autocomplete: :off, optional: true
field trusted_publisher_form, :text_field, :workflow_repository_owner, autocomplete: :off, optional: true
field trusted_publisher_form, :text_field, :workflow_repository_name, autocomplete: :off, optional: true
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ def view_template
dt(class: "description__heading ") { "Workflow Filename" }
dd { code { github_action.workflow_filename } }

if github_action.workflow_repository_owner.present?
dt(class: "description__heading") { "Workflow Repository" }
dd { code { github_action.workflow_repository } }
end

if github_action.environment?
dt(class: "description__heading") { "Environment" }
dd { code { github_action.environment } }
Expand Down
4 changes: 4 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ de:
api_key_permissions: API-Schlüsselberechtigungen
oidc/trusted_publisher/github_action:
repository_owner_id: GitHub Repository-Besitzer-ID
workflow_repository_owner:
workflow_repository_name:
oidc/pending_trusted_publisher:
rubygem_name: RubyGem-Name
errors:
Expand Down Expand Up @@ -1042,6 +1044,8 @@ de:
repository_name_help_html:
workflow_filename_help_html:
environment_help_html:
workflow_repository_owner_help_html:
workflow_repository_name_help_html:
pending:
rubygem_name_help_html:
duration:
Expand Down
8 changes: 8 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ en:
api_key_permissions: API Key Permissions
oidc/trusted_publisher/github_action:
repository_owner_id: GitHub Repository Owner ID
workflow_repository_owner: Workflow Repository Owner
workflow_repository_name: Workflow Repository Name
oidc/pending_trusted_publisher:
rubygem_name: RubyGem name
errors:
Expand Down Expand Up @@ -971,6 +973,12 @@ en:
The name of the <a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment">GitHub Actions environment</a> that the above workflow uses for publishing.<br>
This should be configured under the repository's settings.<br>
While not required, a dedicated publishing environment is strongly encouraged, especially if your repository has maintainers with commit access who shouldn't have RubyGems.org gem push access.
workflow_repository_owner_help_html: |
<strong>For <a href="https://docs.github.com/en/actions/sharing-automations/reusing-workflows">reusable workflows</a> only:</strong> The GitHub organization or username that owns the repository containing the reusable workflow file.<br>
Leave blank if the workflow is defined in the same repository as above (the common case).
workflow_repository_name_help_html: |
<strong>For <a href="https://docs.github.com/en/actions/sharing-automations/reusing-workflows">reusable workflows</a> only:</strong> The name of the repository containing the reusable workflow file.<br>
Leave blank if the workflow is defined in the same repository as above (the common case).
pending:
rubygem_name_help_html: "The gem (on RubyGems.org) that will be created when this publisher is used"
duration:
Expand Down
4 changes: 4 additions & 0 deletions config/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ es:
api_key_permissions:
oidc/trusted_publisher/github_action:
repository_owner_id:
workflow_repository_owner:
workflow_repository_name:
oidc/pending_trusted_publisher:
rubygem_name:
errors:
Expand Down Expand Up @@ -1088,6 +1090,8 @@ es:
repository_name_help_html:
workflow_filename_help_html:
environment_help_html:
workflow_repository_owner_help_html:
workflow_repository_name_help_html:
pending:
rubygem_name_help_html:
duration:
Expand Down
4 changes: 4 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ fr:
api_key_permissions:
oidc/trusted_publisher/github_action:
repository_owner_id:
workflow_repository_owner:
workflow_repository_name:
oidc/pending_trusted_publisher:
rubygem_name:
errors:
Expand Down Expand Up @@ -999,6 +1001,8 @@ fr:
repository_name_help_html:
workflow_filename_help_html:
environment_help_html:
workflow_repository_owner_help_html:
workflow_repository_name_help_html:
pending:
rubygem_name_help_html:
duration:
Expand Down
4 changes: 4 additions & 0 deletions config/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ ja:
api_key_permissions: APIキーのパーミッション
oidc/trusted_publisher/github_action:
repository_owner_id: GitHubリポジトリの所有者ID
workflow_repository_owner:
workflow_repository_name:
oidc/pending_trusted_publisher:
rubygem_name: RubyGem名
errors:
Expand Down Expand Up @@ -990,6 +992,8 @@ ja:
これはリポジトリの設定で構成されていると良いでしょう。<br>
必須ではありませんが、個別の公開環境は強く推奨されます。
特にリポジトリに、コミットアクセスを持つがRubyGems.orgへのgemのプッシュアクセスを持つべきではないメンテナがいるときが該当します。
workflow_repository_owner_help_html:
workflow_repository_name_help_html:
pending:
rubygem_name_help_html: "(RubyGems.orgの)gemはこの発行元が使われたときに作られます"
duration:
Expand Down
4 changes: 4 additions & 0 deletions config/locales/nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ nl:
api_key_permissions:
oidc/trusted_publisher/github_action:
repository_owner_id:
workflow_repository_owner:
workflow_repository_name:
oidc/pending_trusted_publisher:
rubygem_name:
errors:
Expand Down Expand Up @@ -956,6 +958,8 @@ nl:
repository_name_help_html:
workflow_filename_help_html:
environment_help_html:
workflow_repository_owner_help_html:
workflow_repository_name_help_html:
pending:
rubygem_name_help_html:
duration:
Expand Down
4 changes: 4 additions & 0 deletions config/locales/pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ pt-BR:
api_key_permissions:
oidc/trusted_publisher/github_action:
repository_owner_id:
workflow_repository_owner:
workflow_repository_name:
oidc/pending_trusted_publisher:
rubygem_name:
errors:
Expand Down Expand Up @@ -978,6 +980,8 @@ pt-BR:
repository_name_help_html:
workflow_filename_help_html:
environment_help_html:
workflow_repository_owner_help_html:
workflow_repository_name_help_html:
pending:
rubygem_name_help_html:
duration:
Expand Down
4 changes: 4 additions & 0 deletions config/locales/zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ zh-CN:
api_key_permissions:
oidc/trusted_publisher/github_action:
repository_owner_id:
workflow_repository_owner:
workflow_repository_name:
oidc/pending_trusted_publisher:
rubygem_name:
errors:
Expand Down Expand Up @@ -970,6 +972,8 @@ zh-CN:
repository_name_help_html:
workflow_filename_help_html:
environment_help_html:
workflow_repository_owner_help_html:
workflow_repository_name_help_html:
pending:
rubygem_name_help_html:
duration:
Expand Down
4 changes: 4 additions & 0 deletions config/locales/zh-TW.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ zh-TW:
api_key_permissions: API 金鑰權限
oidc/trusted_publisher/github_action:
repository_owner_id:
workflow_repository_owner:
workflow_repository_name:
oidc/pending_trusted_publisher:
rubygem_name:
errors:
Expand Down Expand Up @@ -958,6 +960,8 @@ zh-TW:
repository_name_help_html:
workflow_filename_help_html:
environment_help_html:
workflow_repository_owner_help_html:
workflow_repository_name_help_html:
pending:
rubygem_name_help_html:
duration:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddWorkflowRepositoryToOIDCTrustedPublisherGitHubActions < ActiveRecord::Migration[8.0]
def change
change_table :oidc_trusted_publisher_github_actions, bulk: true do |t|
t.string :workflow_repository_owner
t.string :workflow_repository_name
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class AddWorkflowRepositoryToUniqueIndex < ActiveRecord::Migration[8.0]
disable_ddl_transaction!

OLD_COLUMNS = %i[repository_owner repository_name repository_owner_id workflow_filename environment].freeze
NEW_COLUMNS = %i[repository_owner repository_name repository_owner_id workflow_filename environment
workflow_repository_owner workflow_repository_name].freeze

def up
remove_index :oidc_trusted_publisher_github_actions,
OLD_COLUMNS,
name: "index_oidc_trusted_publisher_github_actions_claims",
algorithm: :concurrently

add_index :oidc_trusted_publisher_github_actions,
NEW_COLUMNS,
name: "index_oidc_trusted_publisher_github_actions_claims",
unique: true,
algorithm: :concurrently
end

def down
remove_index :oidc_trusted_publisher_github_actions,
NEW_COLUMNS,
name: "index_oidc_trusted_publisher_github_actions_claims",
algorithm: :concurrently

add_index :oidc_trusted_publisher_github_actions,
OLD_COLUMNS,
name: "index_oidc_trusted_publisher_github_actions_claims",
unique: true,
algorithm: :concurrently
end
end
6 changes: 4 additions & 2 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.0].define(version: 2025_11_06_032329) do
ActiveRecord::Schema[8.0].define(version: 2026_01_07_200001) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
enable_extension "pg_catalog.plpgsql"
Expand Down Expand Up @@ -492,7 +492,9 @@
t.string "environment"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["repository_owner", "repository_name", "repository_owner_id", "workflow_filename", "environment"], name: "index_oidc_trusted_publisher_github_actions_claims", unique: true
t.string "workflow_repository_owner"
t.string "workflow_repository_name"
t.index ["repository_owner", "repository_name", "repository_owner_id", "workflow_filename", "environment", "workflow_repository_owner", "workflow_repository_name"], name: "index_oidc_trusted_publisher_github_actions_claims", unique: true
end

create_table "organization_invites", force: :cascade do |t|
Expand Down
Loading
Loading