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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +16 -1
- data/lib/deployangel/cli/formatter.rb +3 -1
- data/lib/deployangel/cli.rb +71 -7
- data/lib/deployangel/client.rb +9 -3
- data/lib/deployangel/credentials.rb +54 -0
- data/lib/deployangel/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66a47a8fb6c86749fb8d29333b9f9f01c7d0c6512dfc7bea7c93fd7191d231a2
|
|
4
|
+
data.tar.gz: 93c324ea15c7ddc1e361d82c922a9543c9d15e23511206f93586e6fa1c7b9c4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/deployangel/cli.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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"] ]
|
|
357
|
-
|
|
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
|
-
@
|
|
580
|
-
|
|
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)
|
data/lib/deployangel/client.rb
CHANGED
|
@@ -22,8 +22,11 @@ module DeployAngel
|
|
|
22
22
|
|
|
23
23
|
attr_reader :endpoint
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
data/lib/deployangel/version.rb
CHANGED
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.
|
|
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
|