deployangel 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05e4cde0e2b605e294d51c73f25af3a2b5d9496c6a8e0b3270c358a9a5b9e4ec
4
- data.tar.gz: 722541542ff63d2cb8eedb0020a974a71ecf4eaaff3338e50e1002bc88e7ebbd
3
+ metadata.gz: 480a4d0a0e8ff4206659a86d40ea0fbfb2c5cc20f55e205aa5a724914add1acf
4
+ data.tar.gz: bdd81c91faac1642d5f78633ab48bd2efb80e15d6a8b3022d4a0d8e18c967f4f
5
5
  SHA512:
6
- metadata.gz: c9674f14d5c5f09540e2fe9a102fd1e16571c6492214a47b70a155bb6c70c98af1e333c58734b8eca2079aab323aca6090a47bf8b964111a59e6734dcee316d3
7
- data.tar.gz: 71fad66b1e6f4d36b1794e8f403b05524a0b151a2b4232067e39f3f7341c80d6673b28362215bdf2250ef179640124249c397ec366c85556a9eae5f9d87ce10a
6
+ metadata.gz: e669f7ad136b1328c7f672120dc00b7f5afb20a09a6ca7b27a37c25e26709f678aec6c5fb24dcc1de68c2ffd04a73e1687d2132f09c94ea95cabf72d4731c597
7
+ data.tar.gz: 88e02d9c250287ac051b233bada6a7469507860aa62cc03390dee8b566770eba38c79431b0e882778b9351a9ca59d03c68b26c8d9cd188c390ee65ad0a4093e8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.27 (2026-10-12)
4
+
5
+ - The MCP server's instructions, and the ones `deployangel install agents`
6
+ writes, tell coding agents that a medium-confidence clearance is a full
7
+ clearance and to report warnings as "watching closely" with the verdict's
8
+ new `watching_closely` list, rather than calling a release "suspicious" or
9
+ its confidence "only medium".
10
+
11
+ ## 0.1.26 (2026-10-11)
12
+
13
+ - `deployangel setup --code=CODE` redeems a one-time code from "Set up with
14
+ your coding agent" and saves the app's token and a "CI deploys" token to
15
+ `.deployangel/credentials.json` (readable only by you, kept out of git),
16
+ without printing either, so tokens stay out of coding agents' chats. The
17
+ command and MCP server use the saved token when `DEPLOYANGEL_API_TOKEN`
18
+ isn't set. `deployangel token app|api` prints a saved token for piping into
19
+ a host's or CI's secrets.
20
+
3
21
  ## 0.1.25 (2026-10-10)
4
22
 
5
23
  - `verify` prints what a clearance doesn't cover right under it: app-wide
data/README.md CHANGED
@@ -430,6 +430,20 @@ with the `verifications:read` scope (plus `deployments` to register deploys or
430
430
  report checks). Never use the production telemetry token on a developer
431
431
  machine.
432
432
 
433
+ To let a coding agent set DeployAngel up, use "Set up with your coding agent"
434
+ on the app's page in DeployAngel: it gives a one-time setup code, not tokens.
435
+ `deployangel setup --code=CODE` redeems it and saves the app's token and a "CI
436
+ deploys" token to `.deployangel/credentials.json`, readable only by you and
437
+ kept out of git, without printing either. The command and MCP server then use
438
+ the saved token whenever `DEPLOYANGEL_API_TOKEN` isn't set. `deployangel token
439
+ app` (or `api`) prints a saved token for piping into a host's or CI's
440
+ secrets, so it never appears on screen or in an agent's conversation:
441
+
442
+ ```bash
443
+ fly secrets set DEPLOYANGEL_TOKEN=$(bundle exec deployangel token app)
444
+ bundle exec deployangel token api | gh secret set DEPLOYANGEL_API_TOKEN
445
+ ```
446
+
433
447
  ```bash
434
448
  export DEPLOYANGEL_API_TOKEN=da_live_...
435
449
 
@@ -8,6 +8,7 @@ require "optparse"
8
8
  require_relative "version"
9
9
  require_relative "configuration"
10
10
  require_relative "client"
11
+ require_relative "credentials"
11
12
  require_relative "ci_environment"
12
13
  require_relative "verification_waiter"
13
14
  require_relative "cli/formatter"
@@ -19,7 +20,7 @@ module DeployAngel
19
20
  # Runs without booting Rails.
20
21
  class CLI
21
22
  USAGE_ERROR = 5
22
- COMMANDS = %w[release verify status plan exercise exception check install doctor mcp version help].freeze
23
+ COMMANDS = %w[release verify status plan exercise exception check install doctor setup token mcp version help].freeze
23
24
 
24
25
  HELP = <<~TEXT
25
26
  Usage: deployangel <command> [options]
@@ -42,13 +43,18 @@ module DeployAngel
42
43
  or set up coding agents deployangel install agents
43
44
  doctor Check the setup: this checkout [--format=text|json]
44
45
  and what DeployAngel has seen
46
+ setup Redeem a one-time setup code deployangel setup --code=CODE
47
+ and save the app's tokens here, without printing them
48
+ token Print a saved token, to pipe deployangel token app|api
49
+ into a host's or CI's secrets
45
50
  mcp Run the MCP server over stdio (for coding agents)
46
51
 
47
52
  With no target, verify, plan, and check use the current git HEAD commit.
48
53
  In GitHub Actions, GitLab CI, CircleCI, and Buildkite, release fills in the
49
54
  commit, a build label (e.g. run-123), and a link to the run automatically.
50
55
  In a Kamal hook, it registers Kamal's release (KAMAL_VERSION).
51
- Environment: DEPLOYANGEL_API_TOKEN (required), DEPLOYANGEL_URL (default #{Configuration::DEFAULT_ENDPOINT})
56
+ Token: DEPLOYANGEL_API_TOKEN, else the one `deployangel setup` saved in .deployangel/.
57
+ Environment: DEPLOYANGEL_URL (default #{Configuration::DEFAULT_ENDPOINT})
52
58
 
53
59
  Exit codes: 0 verified, 1 failed, 2 inconclusive, 3 still in progress / timed out,
54
60
  4 deployment not found, 5 usage, auth, or network error,
@@ -110,6 +116,10 @@ module DeployAngel
110
116
  plan's read-only requests and records them on the release. Offer to
111
117
  exercise what it skips: routes that change data only with a test account
112
118
  or after asking. If the status is "warm_up", nothing run can clear it.
119
+ - Confidence `medium` clears a release just as `high` does (high also needs 80%
120
+ coverage and a full baseline), so don't call a medium clearance weak.
121
+ Warnings (`suspicious: true`) aren't failures: report them as "watching
122
+ closely:" and the `watching_closely` items, never as "suspicious".
113
123
  #{AGENTS_END}
114
124
  MARKDOWN
115
125
 
@@ -153,6 +163,8 @@ module DeployAngel
153
163
  when "check" then check
154
164
  when "install" then install
155
165
  when "doctor" then doctor
166
+ when "setup" then setup
167
+ when "token" then token
156
168
  when "mcp" then mcp
157
169
  when "version", "--version", "-v" then @stdout.puts(DeployAngel::VERSION) || 0
158
170
  when nil, "help", "--help", "-h" then @stdout.puts(HELP) || 0
@@ -354,8 +366,9 @@ module DeployAngel
354
366
  def doctor_client
355
367
  return @client if @client
356
368
 
357
- token = [ @env["DEPLOYANGEL_API_TOKEN"], @env["DEPLOYANGEL_TOKEN"] ].find { |value| !value.to_s.empty? } or return
358
- Client.new(token: token, endpoint: @env["DEPLOYANGEL_URL"] || Configuration::DEFAULT_ENDPOINT)
369
+ token = [ @env["DEPLOYANGEL_API_TOKEN"], @env["DEPLOYANGEL_TOKEN"], saved_credentials["api_token"] ]
370
+ .find { |value| !value.to_s.empty? } or return
371
+ Client.new(token: token, endpoint: endpoint)
359
372
  end
360
373
 
361
374
  def install
@@ -576,9 +589,63 @@ module DeployAngel
576
589
  nil
577
590
  end
578
591
 
592
+ # DEPLOYANGEL_API_TOKEN, else the token `deployangel setup` saved here.
579
593
  def client
580
- @client ||= Client.new(token: @env["DEPLOYANGEL_API_TOKEN"],
581
- endpoint: @env["DEPLOYANGEL_URL"] || Configuration::DEFAULT_ENDPOINT)
594
+ token = @env["DEPLOYANGEL_API_TOKEN"].to_s.empty? ? saved_credentials["api_token"] : @env["DEPLOYANGEL_API_TOKEN"]
595
+ @client ||= Client.new(token: token, endpoint: endpoint)
596
+ end
597
+
598
+ def endpoint
599
+ @env["DEPLOYANGEL_URL"].to_s.empty? ? (saved_credentials["endpoint"] || Configuration::DEFAULT_ENDPOINT) : @env["DEPLOYANGEL_URL"]
600
+ end
601
+
602
+ def saved_credentials
603
+ @saved_credentials ||= Credentials.read(@root)
604
+ end
605
+
606
+ # Redeems a one-time code from "Set up with your coding agent" for the
607
+ # app's two tokens, and saves them without printing either, so they
608
+ # stay out of a coding agent's conversation.
609
+ def setup
610
+ options = parse(code: nil) { |o, opts| o.on("--code=CODE") { |v| opts[:code] = v } }
611
+ return usage_error("setup needs --code (from \"Set up with your coding agent\" on the app's page)") if options[:code].to_s.empty?
612
+
613
+ redeemer = @client || Client.new(token: nil, endpoint: endpoint, require_token: false)
614
+ result = redeemer.redeem_setup_code(options[:code].strip)
615
+ app = result["app"] || {}
616
+ file = Credentials.write(@root, { "app" => app, "app_token" => result["app_token"], "api_token" => result["api_token"],
617
+ "endpoint" => endpoint })
618
+ @saved_credentials = nil
619
+ relative = file.delete_prefix("#{@root}/")
620
+ @stdout.puts(<<~TEXT)
621
+ Set up DeployAngel for #{app["name"]} (#{app["environment"]}). Saved its app token and a "CI deploys" token to
622
+ #{relative}, kept out of git and not shown here. The deployangel command and MCP server here use them.
623
+
624
+ Give production the app token without printing it, for example:
625
+ fly secrets set DEPLOYANGEL_TOKEN=$(#{command_name} token app)
626
+ heroku config:set DEPLOYANGEL_TOKEN=$(#{command_name} token app)
627
+ and CI the "CI deploys" token:
628
+ #{command_name} token api | gh secret set DEPLOYANGEL_API_TOKEN
629
+ TEXT
630
+ 0
631
+ end
632
+
633
+ # Prints a saved token, for piping into a host's or CI's secrets. Never
634
+ # run it bare where its output is shown, such as in an agent's chat.
635
+ def token
636
+ kind = @argv.shift
637
+ return usage_error("token needs app or api: deployangel token app") unless %w[app api].include?(kind)
638
+
639
+ value = saved_credentials["#{kind}_token"]
640
+ return usage_error("no token saved here; run deployangel setup --code=<code> first") if value.to_s.empty?
641
+
642
+ @stdout.print(value)
643
+ @stdout.puts if @stdout.tty?
644
+ 0
645
+ end
646
+
647
+ def command_name
648
+ File.exist?(File.join(@root, "Gemfile")) ? "bundle exec deployangel" : "deployangel"
582
649
  end
583
650
 
584
651
  def usage_error(message)
@@ -22,8 +22,11 @@ module DeployAngel
22
22
 
23
23
  attr_reader :endpoint
24
24
 
25
- def initialize(token:, endpoint: Configuration::DEFAULT_ENDPOINT, timeout: 15)
26
- raise Unauthorized, "DEPLOYANGEL_API_TOKEN is not set" if token.to_s.empty?
25
+ # token: nil only for redeeming a setup code, which is its own credential.
26
+ def initialize(token:, endpoint: Configuration::DEFAULT_ENDPOINT, timeout: 15, require_token: true)
27
+ if require_token && token.to_s.empty?
28
+ raise Unauthorized, "DEPLOYANGEL_API_TOKEN is not set, and no token was saved here by `deployangel setup`"
29
+ end
27
30
 
28
31
  @token = token
29
32
  @endpoint = endpoint.to_s.chomp("/")
@@ -41,6 +44,9 @@ module DeployAngel
41
44
  get("/api/v1/deployments/#{deployment_id}/verification", all_findings ? { findings: "all" } : {})
42
45
  end
43
46
 
47
+ # The app's token and a "CI deploys" token, for a one-time setup code.
48
+ def redeem_setup_code(code) = post("/api/v1/setup_codes/redeem", { code: code })
49
+
44
50
  def register_deployment(commit: nil, version: nil, kind: nil, provider: nil, source_url: nil)
45
51
  post("/api/v1/deployments", { commit: commit, version: version, kind: kind, provider: provider, source_url: source_url }.compact)
46
52
  end
@@ -71,7 +77,7 @@ module DeployAngel
71
77
  end
72
78
 
73
79
  def request(request)
74
- request["Authorization"] = "Bearer #{@token}"
80
+ request["Authorization"] = "Bearer #{@token}" unless @token.to_s.empty?
75
81
  request["Accept"] = "application/json"
76
82
  request["User-Agent"] = "deployangel-cli/#{DeployAngel::VERSION}"
77
83
  uri = request.uri
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module DeployAngel
7
+ # Tokens `deployangel setup` saved for a project, so they never have to be
8
+ # printed, pasted into a coding agent's chat, or kept in the environment:
9
+ # .deployangel/credentials.json, readable only by its owner. The directory
10
+ # carries its own .gitignore, so it stays out of git without touching the
11
+ # project's.
12
+ module Credentials
13
+ DIR = ".deployangel"
14
+ FILE = "credentials.json"
15
+
16
+ module_function
17
+
18
+ def path(root)
19
+ File.join(root, DIR, FILE)
20
+ end
21
+
22
+ # From root or a parent directory, as git finds .git.
23
+ def find(root)
24
+ dir = File.expand_path(root)
25
+ loop do
26
+ candidate = path(dir)
27
+ return candidate if File.file?(candidate)
28
+
29
+ parent = File.dirname(dir)
30
+ return nil if parent == dir
31
+
32
+ dir = parent
33
+ end
34
+ end
35
+
36
+ def read(root)
37
+ file = find(root) or return {}
38
+ data = JSON.parse(File.read(file))
39
+ data.is_a?(Hash) ? data : {}
40
+ rescue JSON::ParserError, SystemCallError
41
+ {}
42
+ end
43
+
44
+ def write(root, data)
45
+ dir = File.join(root, DIR)
46
+ FileUtils.mkdir_p(dir)
47
+ File.write(File.join(dir, ".gitignore"), "*\n")
48
+ file = path(root)
49
+ File.open(file, File::WRONLY | File::CREAT | File::TRUNC, 0o600) { |io| io.write(JSON.pretty_generate(data)) }
50
+ File.chmod(0o600, file)
51
+ file
52
+ end
53
+ end
54
+ end
@@ -19,6 +19,10 @@ module DeployAngel
19
19
  with until "initial" (about 15 minutes) or "verdict". Verdicts: verified means cleared, so you may
20
20
  report the release as successful. inconclusive means NOT verified: never report it as success.
21
21
  failed means production regressed: read the findings and exceptions and investigate.
22
+ Confidence medium clears a release just as high does (high also needs 80% coverage and a full
23
+ baseline): never call a medium clearance weak. suspicious true means warnings DeployAngel is
24
+ watching closely, not a failure: report them as "watching closely:" and the watching_closely items,
25
+ never as "suspicious".
22
26
  Findings are deterministic evidence; any "investigation" field is AI inference. These tools cannot
23
27
  change production. Do not roll back or change production without explicit approval.
24
28
  If a release isn't cleared yet, get_exercise_plan says what to exercise against production so it
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.25"
4
+ VERSION = "0.1.27"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deployangel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.25
4
+ version: 0.1.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens
@@ -46,6 +46,7 @@ files:
46
46
  - lib/deployangel/core/redaction.rb
47
47
  - lib/deployangel/core/release.rb
48
48
  - lib/deployangel/core/transport.rb
49
+ - lib/deployangel/credentials.rb
49
50
  - lib/deployangel/doctor.rb
50
51
  - lib/deployangel/exerciser.rb
51
52
  - lib/deployangel/fork_hook.rb