Class: Inquirex::Tools::Publisher

Inherits:
Object
  • Object
show all
Defined in:
lib/inquirex/tools/publisher.rb

Overview

Publishes every package in the family by running each one's own just publish in its own directory.

How a package reaches its registry stays that package's business: the gems build a .gem and gem push it, the npm packages run bun or yarn. This class only decides the order, supplies the one thing a package cannot work out for itself — a fresh 2FA code — and stops at the first failure.

A TOTP is single-use. Reading 1Password once per gem inside a 30-second window returns the same six digits every time, and RubyGems rejects the second push as a replay. So a code is read per gem and the run waits for it to actually rotate before moving on, which is why publishing four gems takes minutes rather than seconds. The npm packages read their own code and are called with no argument.

Order comes from Workspace::LOCKSTEP, so adding a package to the family adds it to the release. That order puts the core gem first, which matters: inquirex-llm and inquirex-tty depend on it and cannot resolve until it is on RubyGems.

Examples:

Preview a release without touching a registry

Inquirex::Tools::Publisher.new.call(dry_run: true)

Constant Summary collapse

ROTATE_TIMEOUT =

How long to wait for a TOTP to rotate before giving up and using what 1Password last returned. Codes rotate every 30 seconds; 90 covers a window boundary landing badly without hanging a release indefinitely.

90
ROTATE_INTERVAL =

Seconds between polls of 1Password while waiting for a new code.

2
DEFAULT_RUNNER =

Shells out with the child's stdout and stderr left attached, so a failing package's own output is what the operator sees.

lambda { |dir, argv|
  Dir.chdir(dir) { system("just", *argv) }
}
DEFAULT_OTP_READER =

Reads the current RubyGems TOTP. op resolves the account from OP_ACCOUNT, which just publish-all exports by decrypting .env.encrypted. Returns nil when unavailable, so a release without 1Password degrades to gem push prompting rather than failing.

lambda {
  ref = "op://open-source-repos/ruby-gems/one-time password?attribute=otp"
  code = `op read #{ref.inspect} 2>/dev/null`.strip
  code.empty? ? nil : code
}
DEFAULT_PUBLISHED_CHECKER =

Asks the registry whether a version is already public.

Returns nil rather than false when the answer cannot be obtained — no network, a 500, a timeout. The caller treats nil as "attempt it": the registry itself refuses a duplicate, so guessing "not published" and trying is safe, whereas guessing "published" would silently skip a package that never shipped.

lambda { |name, kind, version|
  url = case kind
        when :gem then "https://rubygems.org/api/v2/rubygems/#{name}/versions/#{version}.json"
        when :npm then "https://registry.npmjs.org/#{name}/#{version}"
        end
  begin
    uri = URI.parse(url)
    response = Net::HTTP.start(uri.host,
      uri.port,
      use_ssl:      true,
      open_timeout: 5,
      read_timeout: 5) do |http|
      http.head(uri.request_uri)
    end
    case response
    when Net::HTTPSuccess  then true
    when Net::HTTPNotFound then false
    end
  rescue StandardError
    nil
  end
}

Instance Method Summary collapse

Constructor Details

#initialize(workspace: Workspace.new, out: $stdout, runner: DEFAULT_RUNNER, otp_reader: DEFAULT_OTP_READER, published_checker: DEFAULT_PUBLISHED_CHECKER) ⇒ Publisher

Returns a new instance of Publisher.

Parameters:

  • workspace (Workspace) (defaults to: Workspace.new)

    the ecosystem checkout to publish from

  • out (IO) (defaults to: $stdout)

    where report output goes

  • runner (#call) (defaults to: DEFAULT_RUNNER)

    invoked with (dir, argv) to run a command; the default shells out. Injected so specs can assert what would run without publishing anything.

  • otp_reader (#call) (defaults to: DEFAULT_OTP_READER)

    returns the current 2FA code, or nil

  • published_checker (#call) (defaults to: DEFAULT_PUBLISHED_CHECKER)

    (name, kind, version) => true/false/nil



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/inquirex/tools/publisher.rb', line 95

def initialize(workspace: Workspace.new,
  out: $stdout,
  runner: DEFAULT_RUNNER,
  otp_reader: DEFAULT_OTP_READER,
  published_checker: DEFAULT_PUBLISHED_CHECKER)
  @workspace = workspace
  @out = out
  @runner = runner
  @otp_reader = otp_reader
  @published_checker = published_checker
end

Instance Method Details

#call(dry_run: false, args: [], force: false) ⇒ Boolean

Publishes every lockstep package, or prints what it would publish.

Idempotent: a package whose current version is already on its registry is skipped, so re-running after a mid-release failure picks up exactly where it stopped instead of throwing six rejections at two registries.

Parameters:

  • dry_run (Boolean) (defaults to: false)

    print the commands and run nothing

  • args (Array<String>) (defaults to: [])

    extra arguments forwarded verbatim to each package's just publish

  • force (Boolean) (defaults to: false)

    attempt every package even when it looks published

Returns:

  • (Boolean)

    true when every package published, skipped or printed



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/inquirex/tools/publisher.rb', line 118

def call(dry_run: false, args: [], force: false)
  out.puts(dry_run ? "DRY RUN — these commands would run, in this order:" : "Publishing the family:")

  last_otp = nil
  published = 0
  skipped = 0

  Workspace::LOCKSTEP.each do |name, kind|
    dir = File.join(workspace.root, name)
    raise Error, "no checkout for #{name} at #{dir}" unless Dir.exist?(dir)

    version = workspace.version_of(name)
    if !force && version && published_checker.call(name, kind, version)
      row(name, kind, "already #{version} on #{registry(kind)} — skipping".dark)
      skipped += 1
      next
    end

    argv = ["publish", *args]
    if dry_run
      row(name, kind, "just #{argv.join(" ")}#{"   (#{version})" if version}")
      published += 1
      next
    end

    # Only the gems take a code: each npm package reads its own.
    if kind == :gem
      last_otp = fresh_otp(last_otp)
      argv += [last_otp] if last_otp
    end

    out.puts
    out.puts "━━ #{name} (#{kind}) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    return false unless run(name, dir, argv)

    published += 1
  end

  out.puts
  out.puts summary(dry_run:, published:, skipped:)
  true
end