import: the host a plan lands on comes from the config
The first standing key in data/import/config.yml, and the only place
a host is named: HOST is gone from import:execute.
Assisted-by: GLM-5.3 via pi
diff --git a/AGENTS.md b/AGENTS.md
index c4b8618..9092477 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -37,7 +37,8 @@ rake import:macos:finalize # Finalize the contacts the oldest
# (PLAN=dir for another; writes Contacts, so
# leave it to the user)
rake import:execute # Land the oldest plan still to land on a host
- # (HOST=https://contacts; PLAN=dir for another)
+ # (host in data/import/config.yml; PLAN=dir for
+ # another)
rake import:status # Summarize the plans in data/import and what
# execute and finalize would carry next
rake profile:install # Download carddav.mobileconfig from the app and
diff --git a/docs/plans/2026-09-19-import-under-data-import.md b/docs/plans/2026-09-19-import-under-data-import.md
index f480c92..2e35bde 100644
--- a/docs/plans/2026-09-19-import-under-data-import.md
+++ b/docs/plans/2026-09-19-import-under-data-import.md
@@ -12,6 +12,11 @@ data/import/
└── done/ # plans filed away, every contact off this Mac
```
+The config's first key is `host`: the base URL `import:execute`
+lands on, and the only place a host is named — command line included —
+so a plan cannot be aimed at another server by accident. Finalize
+takes no host at all, reading the one its plan recorded.
+
Before, plans lived in `data/imports` beside nothing. Everything the
tasks keep now sits under the one directory, plans in flight under
`active/` and finished under `done/` — the two states a plan's
diff --git a/tasks/import.rake b/tasks/import.rake
index 7ece2cf..a5ee47f 100644
--- a/tasks/import.rake
+++ b/tasks/import.rake
@@ -5,6 +5,9 @@
require "fileutils"
require "pathname"
+require "uri"
+
+require "pro_tacts/import/config"
# The plan a task carries further. A module rather than task-file methods,
# which rake redefines noisily when a file is loaded twice.
@@ -50,6 +53,21 @@ module ImportTasks
FileUtils.mkdir_p(DONE)
FileUtils.mv(plan.dir, DONE / plan.dir.basename)
end
+
+ # The host to land a plan on: the standing `host` under CONFIG, the
+ # one place a host is named. A bare hostname is the base URL of a
+ # server that serves HTTPS, which every deployment does; the scheme
+ # is spelled out here so the host a plan records is the one a second
+ # run is compared against.
+ #: () -> String
+ def self.host
+ named = ProTacts::Import::Config.read(CONFIG).fetch("host", nil)
+ abort "no host to land on: write host: under #{CONFIG}" if named.nil?
+
+ uri = URI.parse(named)
+ uri = URI.parse("https://#{uri}") if uri.scheme.nil?
+ uri.to_s
+ end
end
namespace :import do
@@ -77,7 +95,6 @@ namespace :import do
desc "Finalize the contacts the oldest landed plan imported, taking them off this Mac and filing the plan away done (PLAN=dir for another)"
task :finalize do
require "net/http"
- require "uri"
require "pro_tacts/import/http_client"
require "pro_tacts/import/macos"
require "pro_tacts/import/finalize"
@@ -135,21 +152,16 @@ namespace :import do
puts "next: import:macos:finalize would finish #{finalizing.dir.basename}" if finalizing
end
- desc "Land the oldest plan in data/import/active still to land, or the one in PLAN, on the pro-tacts at HOST, a base URL such as https://contacts"
+ desc "Land the oldest plan in data/import/active still to land, or the one in PLAN, on the pro-tacts at the host in data/import/config.yml, a base URL such as https://contacts"
task :execute do
require "net/http"
- require "uri"
require "pro_tacts/import/execute"
require "pro_tacts/import/http_client"
require "pro_tacts/import/plan"
plan = ImportTasks.plan(ImportTasks::TO_LAND, "to land")
- # A bare hostname is the base URL of a server that serves HTTPS, which
- # every deployment does; the scheme is spelled out here so the host
- # the plan records is the one a second run is compared against.
- uri = URI.parse(ENV.fetch("HOST"))
- uri = URI.parse("https://#{uri}") if uri.scheme.nil?
- host = uri.to_s
+ host = ImportTasks.host
+ uri = URI.parse(host)
puts "landing #{plan.dir} on #{host}"
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
diff --git a/test/tasks/test_import.rb b/test/tasks/test_import.rb
index 6d1bb16..c9d8996 100644
--- a/test/tasks/test_import.rb
+++ b/test/tasks/test_import.rb
@@ -6,7 +6,7 @@ require "tmpdir"
require "pro_tacts/import/plan"
-class ImportStatusTaskTest < Minitest::Test
+class ImportTasksTest < Minitest::Test
Plan = ProTacts::Import::Plan
SOURCE = {"identifier" => "A:ABPerson", "vcard" => "BEGIN:VCARD\r\nEND:VCARD\r\n", "note" => nil, "contact" => {}}
@@ -29,7 +29,7 @@ class ImportStatusTaskTest < Minitest::Test
underway.record("vmnlryyvktux", Plan::DONE)
Plan.write(plans / "macos-20260916T180412Z", source: "macos", created_at: Time.utc(2026, 9, 16, 18, 4, 12), entries: [entry("aaaaaaaaaaaa")])
- out, = run_task(root)
+ out, = run_status(root)
assert_includes out, "macos-20260901T000000Z 2/2 contacts\n"
assert_includes out, "macos-20260916T180412Z 0/1 contacts\n"
@@ -43,22 +43,52 @@ class ImportStatusTaskTest < Minitest::Test
def test_status_with_no_plans_says_how_to_build_one
Dir.mktmpdir do |root|
- out, = run_task(Pathname.new(root))
+ out, = run_status(Pathname.new(root))
assert_includes out, "no plans in data/import/active; run rake import:macos:plan"
end
end
+ def test_the_host_comes_from_the_config_and_a_bare_hostname_gains_a_scheme
+ Dir.mktmpdir do |root|
+ root = Pathname.new(root)
+ write_config(root, "host: contacts\n")
+
+ with_import_tasks(root) do
+ assert_equal "https://contacts", ImportTasks.host
+ end
+ end
+ end
+
+ def test_no_host_in_the_config_aborts_naming_it
+ Dir.mktmpdir do |root|
+ with_import_tasks(Pathname.new(root)) do
+ _, err = capture_io { assert_raises(SystemExit) { ImportTasks.host } }
+
+ assert_equal "no host to land on: write host: under data/import/config.yml\n", err
+ end
+ end
+ end
+
private
- # Runs import:status in a Rake application of its own, from root as
- # the working directory: the task reads data/import relative to
+ def write_config(root, content)
+ (root / "data" / "import").mkpath
+ (root / "data" / "import" / "config.yml").write(content)
+ end
+
+ def run_status(root)
+ with_import_tasks(root) { capture_io { Rake.application["import:status"].invoke } }
+ end
+
+ # Loads tasks/import.rake in a Rake application of its own, from root
+ # as the working directory: the tasks read data/import relative to
# where rake runs, not through ProTacts.config.
- def run_task(root)
+ def with_import_tasks(root)
application = Rake.application
Rake.application = Rake::Application.new
load (Pathname.new(__dir__).parent.parent / "tasks" / "import.rake").to_s
- Dir.chdir(root) { capture_io { Rake.application["import:status"].invoke } }
+ Dir.chdir(root) { yield }
ensure
Rake.application = application
end