opencode-ruby-upgrader 0.1.7 → 0.1.8

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.
package/README.md CHANGED
@@ -117,7 +117,7 @@ The agent defaults unknown shell commands to an OpenCode confirmation prompt. Gi
117
117
 
118
118
  New reports require `record-executed-iteration --validation <id>` or `record-executed-rails-iteration --validation <test-id>`; asserted results cannot be recorded or committed. The accepted IDs map to fixed no-shell commands: `bundle-rspec`, `bundle-rails-test`, `bundle-rake-test`, `bin-rails-test`, and, for Rails bridges, `rails-app-update`. When the target Ruby is unavailable on your host, the agent runs `prepare-target-runtime --ruby <x.y.z>` after selecting the exact target patch release. One confirmation provisions labeled per-run Ruby and isolated database Docker resources, installs Node, installs Bundler 2.4.22, runs `bundle install`, and, for Rails, creates the isolated test database.
119
119
 
120
- The isolated test database is PostgreSQL by default. If the project declares MySQL through `mysql2` (the gem or the adapter in `config/database.yml`) and not PostgreSQL, the runtime is prepared with an isolated MySQL 8.4 server instead. The engine is detected from `Gemfile`, `Gemfile.lock`, and `config/database.yml`; the legacy `mysql` and `trilogy` gems are not recognized, and a project that declares both engines stops preparation and asks you to choose:
120
+ The isolated test database is PostgreSQL by default. If the project declares MySQL through `mysql2` and not PostgreSQL, the runtime is prepared with an isolated MySQL 8.4 server instead. Detection reads declarations rather than incidental words: an `adapter:` line in `config/database.yml`, a `gem "mysql2"` / `gem "pg"` line, a resolved spec in `Gemfile.lock`, or a driver URL. `config/database.yml` wins over gem declarations, because a gem line only says a driver is *available* while the YAML says what the app connects to — so a PostgreSQL project that keeps `gem "mysql2"` for an unrelated import tool still resolves to PostgreSQL. The legacy `mysql` and `trilogy` gems are not recognized, and a project that declares both engines stops preparation and asks you to choose:
121
121
 
122
122
  ```bash
123
123
  opencode-ruby-upgrader prepare-target-runtime --ruby <x.y.z> --database mysql
@@ -5,7 +5,7 @@ Complete every item before pushing a `v*` tag. The protected `npm-release` envir
5
5
  ## Before every release
6
6
 
7
7
  - [ ] Bump the version and update [RELEASE_NOTES.md](RELEASE_NOTES.md): scope, supported adapters, known limitations, rollback.
8
- - [ ] For every newly supported adapter — including a database engine — record real container evidence, not only mocked tests. `npm run test:docker:mysql` provisions a real `mysql:8.4`, connects from the app through its own `mysql2` driver, and checks the receipt. Do not ship a new adapter on unit tests alone. The `mysql-runtime` CI job runs it on every pull request and `release.yml` runs it again before `npm publish`, so this is enforced rather than advisory.
8
+ - [ ] For every newly supported adapter — including a database engine — record real container evidence, not only mocked tests. `npm run test:docker` provisions a real `mysql:8.4` **and** a real `postgres:16-alpine`, connects from each app through its own driver, and checks the receipt. Do not ship a new adapter on unit tests alone. The `adapter-runtime` CI job runs both engines on every pull request and `release.yml` runs them again before `npm publish`, so this is enforced rather than advisory. Prove it for every adapter you ship, not just the one you changed: the adapter that has no container evidence is the one that surprises you.
9
9
  - [ ] Review new or changed CLI options in `opencode-ruby-upgrader <command> --help`; users should not have to read the source to find a documented flag.
10
10
  - [ ] Reconcile README, `agents/ruby-upgrade.md`, and release notes with the shipped behavior: defaults, detection and ambiguity behavior, resource lifecycle, and what is persisted.
11
11
  - [ ] Repoint the README's tag-pinned links to the new version tag: `E2E_EVIDENCE.md`, `PRIVACY.md`, `SECURITY.md`, `docs/rails-bridge.md`, and both screenshot URLs. They are pinned to a tag so the registry page always shows the docs of the installed version rather than whatever `main` currently says; leaving them on the previous tag makes the published page drift behind the release.
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # opencode-ruby-upgrader — release notes
2
2
 
3
+ ## v0.1.8 — real-adapter detection and PostgreSQL container evidence
4
+
5
+ v0.1.7 shipped database detection that had only ever been tested against hand-written fixtures. Running it against real Rails applications found three wrong answers, one of them silent. This release fixes detection and closes the container-evidence gap for the default adapter.
6
+
7
+ **Scope**
8
+
9
+ - Detection now matches declarations rather than tokens. Previously `mysql2` matched anywhere in `Gemfile`, `Gemfile.lock`, or `config/database.yml`, while PostgreSQL required an `adapter:` line. That asymmetry meant only a false *MySQL* answer was possible, and it happened: rails/rails ships `activerecord/test/config.example.yml` with `mysql2:` and `postgresql:` as YAML keys naming a profile under `connections:`, and every adapter actually selected is `sqlite3`. The old rule read the `mysql2:` key and silently reported MySQL. A key that names a profile is not a declaration, so it no longer counts.
10
+ - `config/database.yml` now takes precedence over gem declarations. A `gem` line only says a driver is *available*; the YAML says what the app connects to. Discourse is PostgreSQL throughout its `config/database.yml` while carrying `gem "mysql2"` behind an import-mode conditional — previously reported as ambiguous, forcing an explicit flag on an unambiguous project. Redmine's `database.yml.example` selects `mysql2` for every environment with `postgresql` commented out, while its `Gemfile` conditionally declares both engines; it now resolves to MySQL rather than refusing to answer.
11
+ - `Gemfile.lock` is read as a declaration source in its resolved-spec form (` mysql2 (0.5.6)`, ` pg (1.6.2)`), so a project with a generated or templated `Gemfile` still resolves correctly.
12
+ - Genuinely multi-engine projects still stop and ask. Spree declares both drivers and no `config/database.yml`, and continues to require an explicit choice.
13
+
14
+ **Evidence**
15
+
16
+ `npm run test:docker` now proves **both** adapters against real containers: a real `mysql:8.4` and a real `postgres:16-alpine`, each connected from a throwaway Rails app through its own driver, each asserting a passing RSpec receipt, a secret-free persisted manifest, and its own teardown. Detection is deliberately left to run rather than passed `--database`, so a leg that only worked because it was told the answer could not pass. The CI job is now `adapter-runtime` and `release.yml` runs both engines before publishing.
17
+
18
+ The smoke fixture is now a real Rails application rather than a bare `Gemfile`. That is not cosmetic: PostgreSQL's adapter creates its test database through the app's own `rake db:create`, so it requires a bootable app, while MySQL's creates it server-side and would accept a `Gemfile` alone. The MySQL leg had therefore been passing against a fixture that could never have exercised the PostgreSQL path. The fixture also pins `host: localhost` with a nonexistent user, so a passing run proves `DATABASE_URL` overrides the project's own configuration rather than accidentally agreeing with it.
19
+
20
+ Detection regressions from real projects are unit-tested from the actual file shapes: the rails/rails profile keys, the Discourse import-mode gem, the Redmine commented-out adapter, the Spree ambiguity, and both `Gemfile.lock` forms.
21
+
22
+ **Known limitations**
23
+
24
+ - Prepared containers, networks, and images are not torn down when a run completes or is paused. They persist for reproducibility and are removed manually; the names are recorded in `.ruby-upgrades/runtime.json`. Still no `dispose-target-runtime` command — cleanup remains documented rather than automated, and is the most requested follow-up.
25
+ - Detection is static text matching. A dynamically computed adapter, or a project whose test environment differs from its other environments, may need an explicit `--database`.
26
+ - `--database mysql2://` URLs are emitted for MySQL and `postgresql://` for PostgreSQL; a project relying on driver-specific `database.yml` keys beyond adapter, host, user, and password is not modelled.
27
+
3
28
  ## v0.1.7 — isolated MySQL runtime
4
29
 
5
30
  The isolated validation runtime can now prepare MySQL as well as PostgreSQL.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ruby-upgrader",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "A safe, evidence-driven Ruby runtime upgrade agent and local migration dashboard for OpenCode",
5
5
  "author": "Priscilla Cournoyer (https://github.com/lilla021)",
6
6
  "repository": {
@@ -18,7 +18,9 @@
18
18
  },
19
19
  "scripts": {
20
20
  "test": "node --test tests/*.test.js",
21
- "test:docker:mysql": "node tests/docker-mysql-smoke.mjs",
21
+ "test:docker": "node tests/docker-adapter-smoke.mjs",
22
+ "test:docker:mysql": "node tests/docker-adapter-smoke.mjs mysql",
23
+ "test:docker:postgres": "node tests/docker-adapter-smoke.mjs postgres",
22
24
  "test:opencode": "node tests/opencode-runtime-smoke.mjs"
23
25
  },
24
26
  "allowScripts": {
package/src/databases.js CHANGED
@@ -40,7 +40,11 @@ export const databases = Object.freeze({
40
40
  startArgs: () => ["--env", "POSTGRES_HOST_AUTH_METHOD=trust", "--env", `POSTGRES_DB=${DB_NAME}`, "postgres:16-alpine"],
41
41
  readyArgs: (container) => ["exec", container, "pg_isready", "-U", "postgres", "-d", DB_NAME],
42
42
  // Use the app's own Rails task so framework-specific database setup remains
43
- // consistent with the project's PostgreSQL adapter.
43
+ // consistent with the project's PostgreSQL adapter. DATABASE_URL is already
44
+ // set on the app container and Rails prefers it over database.yml, so a
45
+ // project pinning `host: localhost` or a socket-only DSN still resolves to
46
+ // the isolated container -- no rewriting of the config under test, which
47
+ // would be a source change to the worktree.
44
48
  createArgs: ({ appContainer }) => ["exec", appContainer, "bundle", "exec", "rake", "db:create"],
45
49
  databaseUrl: (container) => `postgresql://postgres@${container}:5432/${DB_NAME}`,
46
50
  ensureReady: ({ probe }) => readyLoop({ probe, attempts: 30, label: "PostgreSQL" })
@@ -75,18 +79,42 @@ export function resolveDatabase(adapter) {
75
79
  return databases[key];
76
80
  }
77
81
 
82
+ // Declarative forms only. A bare word is not a declaration: real
83
+ // `config/database.yml` files carry YAML keys that merely *name* a profile
84
+ // (rails/rails ships `connections:` with `mysql2:` and `postgresql:` keys and
85
+ // only `adapter: sqlite3` in play), and a Gemfile may carry a driver for an
86
+ // unrelated tool. Matching bare tokens made detection asymmetric -- mysql2
87
+ // matched anywhere while postgres required `adapter:` -- so the bias could only
88
+ // ever produce a false MySQL answer, silently.
89
+ const MYSQL = [/\badapter\s*:\s*mysql2\b/i, /\bgem\s*\(?\s*["']mysql2["']/i, /\bmysql2:\/\//i];
90
+ const POSTGRES = [/\badapter\s*:\s*(?:postgresql|postgres)\b/i, /\bgem\s*\(?\s*["']pg["']/i, /\b(?:postgresql|postgres):\/\//i];
91
+ // Gemfile.lock records resolved specs as ` mysql2 (0.5.6)` / ` pg (1.6.2)`.
92
+ const MYSQL_LOCK = /^[ \t]*mysql2 \(/m;
93
+ const POSTGRES_LOCK = /^[ \t]*pg \(/m;
94
+
78
95
  // Best-effort adapter detection from the project's own declarations. An
79
96
  // explicit `--database` always wins; this only fills the gap so the common case
80
97
  // needs no flag.
81
98
  export function detectDatabase(root = process.cwd()) {
82
99
  const read = (file) => { try { return fs.readFileSync(path.join(root, file), "utf8"); } catch { return ""; } };
83
100
  const uncommented = (contents) => contents.replace(/#.*$/gm, "");
84
- const corpus = [read("Gemfile"), read("Gemfile.lock"), read("config/database.yml")].map(uncommented).join("\n");
85
- const mysql = /\bmysql2\b/i.test(corpus) || /adapter:\s*mysql2\b/i.test(corpus);
86
- const postgres = /\bpg\b/i.test(corpus) || /adapter:\s*(?:postgresql|postgres)\b/i.test(corpus) || /postgresql:\/\//i.test(corpus);
87
- if (mysql && postgres) throw new Error("Both MySQL and PostgreSQL were detected. Specify --database mysql or --database postgres.");
88
- if (mysql && !postgres) return "mysql";
89
- return "postgres";
101
+ const declares = (contents, lock) => ({
102
+ mysql: MYSQL.some((re) => re.test(contents)) || (lock && MYSQL_LOCK.test(contents)),
103
+ postgres: POSTGRES.some((re) => re.test(contents)) || (lock && POSTGRES_LOCK.test(contents)),
104
+ });
105
+ const resolve = ({ mysql, postgres }) => {
106
+ if (mysql && postgres) throw new Error("Both MySQL and PostgreSQL were detected. Specify --database mysql or --database postgres.");
107
+ return mysql ? "mysql" : postgres ? "postgres" : null;
108
+ };
109
+ // `config/database.yml` is what the app actually connects to; a `gem` line
110
+ // only says a driver is *available*. Discourse, for instance, is PostgreSQL
111
+ // (`adapter: postgresql` throughout) but carries `gem "mysql2"` behind an
112
+ // import-mode conditional. OR-ing both files reported that as ambiguous and
113
+ // needlessly forced an explicit flag on an unambiguous project.
114
+ const configured = resolve(declares(uncommented(read("config/database.yml")), false));
115
+ if (configured) return configured;
116
+ const gems = resolve(declares(`${uncommented(read("Gemfile"))}\n${uncommented(read("Gemfile.lock"))}`, true));
117
+ return gems ?? "postgres";
90
118
  }
91
119
 
92
120
  export { DB_NAME, NAME };