deployangel 0.1.25 → 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 +10 -0
- data/README.md +14 -0
- data/lib/deployangel/cli.rb +69 -6
- 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,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 0.1.25 (2026-10-10)
|
|
4
14
|
|
|
5
15
|
- `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
|
|
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,
|
|
@@ -153,6 +159,8 @@ module DeployAngel
|
|
|
153
159
|
when "check" then check
|
|
154
160
|
when "install" then install
|
|
155
161
|
when "doctor" then doctor
|
|
162
|
+
when "setup" then setup
|
|
163
|
+
when "token" then token
|
|
156
164
|
when "mcp" then mcp
|
|
157
165
|
when "version", "--version", "-v" then @stdout.puts(DeployAngel::VERSION) || 0
|
|
158
166
|
when nil, "help", "--help", "-h" then @stdout.puts(HELP) || 0
|
|
@@ -354,8 +362,9 @@ module DeployAngel
|
|
|
354
362
|
def doctor_client
|
|
355
363
|
return @client if @client
|
|
356
364
|
|
|
357
|
-
token = [ @env["DEPLOYANGEL_API_TOKEN"], @env["DEPLOYANGEL_TOKEN"] ]
|
|
358
|
-
|
|
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)
|
|
359
368
|
end
|
|
360
369
|
|
|
361
370
|
def install
|
|
@@ -576,9 +585,63 @@ module DeployAngel
|
|
|
576
585
|
nil
|
|
577
586
|
end
|
|
578
587
|
|
|
588
|
+
# DEPLOYANGEL_API_TOKEN, else the token `deployangel setup` saved here.
|
|
579
589
|
def client
|
|
580
|
-
@
|
|
581
|
-
|
|
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"
|
|
582
645
|
end
|
|
583
646
|
|
|
584
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
|