deployangel 0.1.24 → 0.1.26

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: 2304a80caf17ea6df84a2487145d51438244f15e38528e817fb93ed45a07dff0
4
- data.tar.gz: b28c3f544144e1b4a8cd9417fbdb4407bd814d814fe88522073e4d463d25f11a
3
+ metadata.gz: 66a47a8fb6c86749fb8d29333b9f9f01c7d0c6512dfc7bea7c93fd7191d231a2
4
+ data.tar.gz: 93c324ea15c7ddc1e361d82c922a9543c9d15e23511206f93586e6fa1c7b9c4c
5
5
  SHA512:
6
- metadata.gz: ba778f41f2922f597a225d2bbdefc58ffce09443ec7f4c342f8df2729103e36a49115bcd76502222a95717eace53b4c737cc836fccff3a865bb52e835501fc0a
7
- data.tar.gz: 98abb8b69f7cf9e61980a68befcff22dee0ede110019a2b6e398a92efaef155e2c700ce3eaf426fb9f3346fcbe0ce320b7bd3ef78979c0459a91bc4a6f0cd263
6
+ metadata.gz: 0ba0355966761d97fc73b3c64eab89f448114c23de7192555913c49d97de57e6349de81eb1fb9994a1a850e170313e8cbed39249fe00d8f3045ba88ea6840fe0
7
+ data.tar.gz: 8d9ca49ffbd98f4feb88e10b94887f171b7bd877e77ff36396ae716defa016ccc29ebc23452475f894f67d149ad3f78421f867ace1644faff8660f7006db2763
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.26 (2026-10-11)
4
+
5
+ - `deployangel setup --code=CODE` redeems a one-time code from "Set up with
6
+ your coding agent" and saves the app's token and a "CI deploys" token to
7
+ `.deployangel/credentials.json` (readable only by you, kept out of git),
8
+ without printing either, so tokens stay out of coding agents' chats. The
9
+ command and MCP server use the saved token when `DEPLOYANGEL_API_TOKEN`
10
+ isn't set. `deployangel token app|api` prints a saved token for piping into
11
+ a host's or CI's secrets.
12
+
13
+ ## 0.1.25 (2026-10-10)
14
+
15
+ - `verify` prints what a clearance doesn't cover right under it: app-wide
16
+ signals without enough data, critical flows and changed items not run
17
+ since the deploy, and latency that rose but stayed within limits (from
18
+ the verdict document's new `clearance.scope`). The coding-agent
19
+ instructions `install agents` writes now say to report it too.
20
+
3
21
  ## 0.1.24 (2026-10-10)
4
22
 
5
23
  - `deployangel doctor` checks the setup: the agent in the bundle, the
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
 
@@ -545,7 +559,8 @@ verdict: call the `wait_for_verification` MCP tool with the commit and
545
559
  `until: "initial"`, calling again while it's in progress, or run
546
560
  `bundle exec deployangel verify --commit=<sha> --wait --until=initial`.
547
561
 
548
- - Exit 0 / verified: the release is cleared. Report the clearance line and
562
+ - Exit 0 / verified: the release is cleared. Report the clearance line, what
563
+ it says it doesn't cover ("Not assessed", "Not run since the deploy"), and
549
564
  anything DeployAngel is still watching, then move on.
550
565
  - Exit 6: no problems so far, but NOT cleared. Report "no problems so far, not
551
566
  yet cleared" and the expected clearance time. DeployAngel keeps verifying and
@@ -70,7 +70,9 @@ module DeployAngel
70
70
 
71
71
  def notes(document)
72
72
  v = document["verification"] || {}
73
- notes = []
73
+ # What a clearance doesn't cover, right under it (servers from
74
+ # 2026-10-10 send it).
75
+ notes = Array(document.dig("clearance", "scope", "lines"))
74
76
  if (check = v["initial_check"]) && v["verdict"].nil?
75
77
  notes << "Initial check: #{check["result"].tr("_", " ")}. Not cleared yet."
76
78
  end
@@ -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,
@@ -91,7 +97,8 @@ module DeployAngel
91
97
  `until: "initial"`, calling again while it's in progress, or run
92
98
  `bundle exec deployangel verify --commit=<sha> --wait --until=initial`.
93
99
 
94
- - Exit 0 / verified: the release is cleared. Report the clearance line and
100
+ - Exit 0 / verified: the release is cleared. Report the clearance line, what
101
+ it says it doesn't cover ("Not assessed", "Not run since the deploy"), and
95
102
  anything DeployAngel is still watching, then move on.
96
103
  - Exit 6: no problems so far, but NOT cleared. Report "no problems so far, not
97
104
  yet cleared" and the expected clearance time. DeployAngel keeps verifying and
@@ -152,6 +159,8 @@ module DeployAngel
152
159
  when "check" then check
153
160
  when "install" then install
154
161
  when "doctor" then doctor
162
+ when "setup" then setup
163
+ when "token" then token
155
164
  when "mcp" then mcp
156
165
  when "version", "--version", "-v" then @stdout.puts(DeployAngel::VERSION) || 0
157
166
  when nil, "help", "--help", "-h" then @stdout.puts(HELP) || 0
@@ -353,8 +362,9 @@ module DeployAngel
353
362
  def doctor_client
354
363
  return @client if @client
355
364
 
356
- token = [ @env["DEPLOYANGEL_API_TOKEN"], @env["DEPLOYANGEL_TOKEN"] ].find { |value| !value.to_s.empty? } or return
357
- Client.new(token: token, endpoint: @env["DEPLOYANGEL_URL"] || Configuration::DEFAULT_ENDPOINT)
365
+ token = [ @env["DEPLOYANGEL_API_TOKEN"], @env["DEPLOYANGEL_TOKEN"], saved_credentials["api_token"] ]
366
+ .find { |value| !value.to_s.empty? } or return
367
+ Client.new(token: token, endpoint: endpoint)
358
368
  end
359
369
 
360
370
  def install
@@ -575,9 +585,63 @@ module DeployAngel
575
585
  nil
576
586
  end
577
587
 
588
+ # DEPLOYANGEL_API_TOKEN, else the token `deployangel setup` saved here.
578
589
  def client
579
- @client ||= Client.new(token: @env["DEPLOYANGEL_API_TOKEN"],
580
- endpoint: @env["DEPLOYANGEL_URL"] || Configuration::DEFAULT_ENDPOINT)
590
+ token = @env["DEPLOYANGEL_API_TOKEN"].to_s.empty? ? saved_credentials["api_token"] : @env["DEPLOYANGEL_API_TOKEN"]
591
+ @client ||= Client.new(token: token, endpoint: endpoint)
592
+ end
593
+
594
+ def endpoint
595
+ @env["DEPLOYANGEL_URL"].to_s.empty? ? (saved_credentials["endpoint"] || Configuration::DEFAULT_ENDPOINT) : @env["DEPLOYANGEL_URL"]
596
+ end
597
+
598
+ def saved_credentials
599
+ @saved_credentials ||= Credentials.read(@root)
600
+ end
601
+
602
+ # Redeems a one-time code from "Set up with your coding agent" for the
603
+ # app's two tokens, and saves them without printing either, so they
604
+ # stay out of a coding agent's conversation.
605
+ def setup
606
+ options = parse(code: nil) { |o, opts| o.on("--code=CODE") { |v| opts[:code] = v } }
607
+ return usage_error("setup needs --code (from \"Set up with your coding agent\" on the app's page)") if options[:code].to_s.empty?
608
+
609
+ redeemer = @client || Client.new(token: nil, endpoint: endpoint, require_token: false)
610
+ result = redeemer.redeem_setup_code(options[:code].strip)
611
+ app = result["app"] || {}
612
+ file = Credentials.write(@root, { "app" => app, "app_token" => result["app_token"], "api_token" => result["api_token"],
613
+ "endpoint" => endpoint })
614
+ @saved_credentials = nil
615
+ relative = file.delete_prefix("#{@root}/")
616
+ @stdout.puts(<<~TEXT)
617
+ Set up DeployAngel for #{app["name"]} (#{app["environment"]}). Saved its app token and a "CI deploys" token to
618
+ #{relative}, kept out of git and not shown here. The deployangel command and MCP server here use them.
619
+
620
+ Give production the app token without printing it, for example:
621
+ fly secrets set DEPLOYANGEL_TOKEN=$(#{command_name} token app)
622
+ heroku config:set DEPLOYANGEL_TOKEN=$(#{command_name} token app)
623
+ and CI the "CI deploys" token:
624
+ #{command_name} token api | gh secret set DEPLOYANGEL_API_TOKEN
625
+ TEXT
626
+ 0
627
+ end
628
+
629
+ # Prints a saved token, for piping into a host's or CI's secrets. Never
630
+ # run it bare where its output is shown, such as in an agent's chat.
631
+ def token
632
+ kind = @argv.shift
633
+ return usage_error("token needs app or api: deployangel token app") unless %w[app api].include?(kind)
634
+
635
+ value = saved_credentials["#{kind}_token"]
636
+ return usage_error("no token saved here; run deployangel setup --code=<code> first") if value.to_s.empty?
637
+
638
+ @stdout.print(value)
639
+ @stdout.puts if @stdout.tty?
640
+ 0
641
+ end
642
+
643
+ def command_name
644
+ File.exist?(File.join(@root, "Gemfile")) ? "bundle exec deployangel" : "deployangel"
581
645
  end
582
646
 
583
647
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.24"
4
+ VERSION = "0.1.26"
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.24
4
+ version: 0.1.26
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