diff --git a/composite-actions/fiscal/openapi-actions/README.md b/composite-actions/fiscal/openapi-actions/README.md new file mode 100644 index 00000000..77bb554f --- /dev/null +++ b/composite-actions/fiscal/openapi-actions/README.md @@ -0,0 +1,87 @@ +# openapi-actions + +Builds the deployable, country-scoped OpenAPI spec for a fiscal adapter, then re-seals the jar so +the running service serves it. In one pass it: + +1. **Materializes** the engine's shared `openapi.yaml` out of the `fiscal-engine` dependency into + the build output — the adapter ships no spec of its own. +2. **Stamps `info.version`** from the optional `version` input (the engine ships it as a literal + `${project.version}` token so each adapter sets its own build version — see below). +3. **Country-filters** it in place: a Redocly decorator strips every other country's + `x-country`-tagged `oneOf` branch, leaving only this country's `attributes` shape. +4. **Re-runs `jar:jar`** so the already-built jar picks up the overwritten resource. + +The engine serves whatever classpath resource ends up at `/schemas/v1/openapi.yaml` verbatim, and +the adapter jar precedes `fiscal-engine.jar` on the classpath — so overwriting that path is enough +to make e.g. Belgium's live service serve a Belgium-only, correctly-versioned spec. **No engine +code change and no per-adapter `pom.xml` config needed.** + +The engine ships one canonical spec whose response `attributes` are `Fiscalize-Attributes` and +`Cancel-Attributes` `oneOf`s — one `x-country`-tagged branch per country +(`src/main/resources/schemas/v1/openapi.yaml` in `hiiretail-fiscal-engine`). + +This is a shared composite action +(`extenda/shared-workflows/composite-actions/fiscal/openapi-actions`), used by all country repos +that fiscalize via `fiscal-engine`. + +## Usage + +Run this any time **after** a normal `mvn package` — it re-seals the jar for you, so callers don't +need to reorder their own build around it. It does no checkout of its own (an `actions/checkout` +step here would run `git clean -ffdx` by default and wipe `target/`), so it must run later in the +same job as the checkout and package steps, not a separate job. + +```yaml +- run: ./mvnw package -DskipTests +- uses: extenda/shared-workflows/composite-actions/fiscal/openapi-actions@v0 + with: + country: belgium + version: ${{ steps.semver.outputs.version }} # optional; omit for local/acceptance runs +- run: docker build -t $IMAGE . # picks up the re-jarred, versioned, Belgium-only target/*.jar +``` + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `country` | yes | – | The lower-case api name from this action's `redocly.yaml` `apis:` map, e.g. `belgium`. | +| `version` | no | `''` | When set, replaces the engine spec's literal `${project.version}` token so the served `info.version` is the adapter's build version. When empty, the token is left as-is (fine for local/acceptance runs, where the version is cosmetic). | +| `schema-dir` | no | `target/classes/schemas/v1` | Directory (relative to the workspace root) the spec is materialized into and filtered in place. | + +## Adding a country + +One identifier throughout: the lower-case country name used as the `apis:` key (e.g. `belgium`) is +reused verbatim as both the decorator's `country` option and the `x-country` tag value — no +separate short code to keep in sync. + +1. In the engine's `openapi.yaml`, add a `Fiscalize-Attributes-` branch to the + `Fiscalize-Attributes` `oneOf` and a `Cancel-Attributes-` branch to the + `Cancel-Attributes` `oneOf`, each tagged `x-country: ` (lower-case, e.g. + `x-country: portugal`). +2. In this action's `redocly.yaml`, add a sibling `apis:` entry named `` with + `openapi/strip-other-countries: { country: }`. + +## Notes / validation status + +- **Version is set by a decorator, not text substitution.** The action forwards the `version` + input into the bundle container as `API_VERSION`; the `openapi/set-version` decorator reads it + and overrides `info.version`, logging `openapi: set info.version = `. Empty ⇒ no-op. +- **Decorators only run during `bundle`, not `lint`** — linting the pre-bundle source wouldn't + exercise the transformation, so this action lints the *bundled* output instead. +- `redocly.yaml` and `decorators/` live in this action's own repo, not the caller's checkout — + `docker run -v $PWD:/spec` only mounts the caller's workspace, so they're staged (`cp`) into + `schema-dir` via `${{ github.action_path }}` before bundling. The staging step clears any + pre-existing `redocly.yaml`/`decorators/` at the destination first, so re-runs on a reused + workspace can't leave a stale or nested `decorators/decorators/` copy behind. +- Bundling reads and writes `openapi.yaml` at the same path — safe because the bundler loads the + whole document graph into memory before it writes any output. +- A single surviving `oneOf` branch (e.g. Belgium today, with only one real country onboarded) is + collapsed by the decorator into the wrapper schema directly, rather than left as a `oneOf` of one. +- `jar:jar` only re-zips already-compiled `target/classes` — no recompile, no test run — so it's + cheap to run as a dedicated step after packaging. +- **Not yet validated against a real country pipeline in CI** — verified locally by running every + step's command (materialize, stage, in-place bundle with `API_VERSION`, lint, `jar:jar`) against + `hiiretail-fiscal-engine`'s freshly-built spec: confirmed the bundle ships both + `Fiscalize-Attributes-BE`/`-EU` and `Cancel-Attributes-BE`/`-EU` before, and only the collapsed + Belgium schemas plus the stamped `info.version` after. Confirm on a first real run in an actual + country pipeline before relying on it for prod. diff --git a/composite-actions/fiscal/openapi-actions/action.yaml b/composite-actions/fiscal/openapi-actions/action.yaml new file mode 100644 index 00000000..47981084 --- /dev/null +++ b/composite-actions/fiscal/openapi-actions/action.yaml @@ -0,0 +1,84 @@ +name: 'Fiscal API country filtering' +description: >- + Materializes the fiscal-engine's shared openapi.yaml from the fiscal-engine dependency, + optionally stamps `info.version`, then replaces it IN PLACE with a country-scoped bundle that + strips every other country's `x-country`-tagged documentation via a Redocly decorator (see the + engine's `Fiscalize-Attributes`/`Cancel-Attributes` oneOf, src/main/resources/schemas/v1/openapi.yaml), + and re-runs `jar:jar` so the already-built jar picks up the change. The engine serves whatever + classpath resource ends up at /schemas/v1/openapi.yaml verbatim — no engine code and no per-country + pom change needed. Run it after a normal `mvn package`. Does no checkout of its own (an + `actions/checkout` step here would run `git clean -ffdx` by default and wipe `target/`). + +inputs: + country: + required: true + description: The lower-case api name from this action's redocly.yaml `apis:` map, e.g. 'belgium'. + version: + required: false + description: >- + When set, replaces the engine spec's literal `${project.version}` token so the served + `info.version` is the adapter's build version. When empty, the token is left as-is. + default: '' + schema-dir: + required: false + description: Directory (relative to the workspace root) containing the built openapi.yaml. + default: 'target/classes/schemas/v1' + +runs: + using: composite + steps: + # The adapter ships no spec of its own — the engine dependency owns openapi.yaml. Unpack it into + # the build output so the steps below (and the re-jar) operate on a real file. Same pattern the + # callers already use for the engine's Liquibase changesets. + - name: Materialize the engine's OpenAPI spec + shell: bash + run: | + ./mvnw -q dependency:unpack-dependencies \ + -DincludeArtifactIds=fiscal-engine \ + -Dincludes=schemas/v1/openapi.yaml \ + -DoutputDirectory=target/openapi-engine-spec + mkdir -p "${{ inputs.schema-dir }}" + cp target/openapi-engine-spec/schemas/v1/openapi.yaml "${{ inputs.schema-dir }}/openapi.yaml" + + # redocly/cli is pinned by digest below for immutability. Dependabot's docker ecosystem only + # scans Dockerfile/docker-compose.yml `FROM`/`image:` fields, not `docker run` invocations + # inside a composite action's shell script — this is NOT auto-updated. Check + # https://hub.docker.com/r/redocly/cli/tags periodically and bump both occurrences by hand. + + # redocly.yaml and decorators/ live in THIS action's own repo (extenda/shared-workflows), not + # the caller's checkout — `docker run -v $PWD:/spec` only mounts the caller's workspace, so + # they have to be staged alongside the spec before redocly can see them. + - name: Stage the country-filter config next to the built spec + shell: bash + run: | + rm -rf "${{ inputs.schema-dir }}/redocly.yaml" "${{ inputs.schema-dir }}/decorators" + cp "${{ github.action_path }}/redocly.yaml" "${{ inputs.schema-dir }}/redocly.yaml" + cp -r "${{ github.action_path }}/decorators" "${{ inputs.schema-dir }}/decorators" + + # Bundles openapi.yaml and overwrites it in place with the country-filtered result — the + # bundler reads the whole document graph before it writes any output, so reading and writing + # the same path is safe. + # API_VERSION is passed through to the container's env (docker `-e NAME` forwards the host + # value); the `set-version` decorator reads it and overrides info.version. Empty => no-op. + - name: Bundle the ${{ inputs.country }} spec in place + shell: bash + env: + API_VERSION: ${{ inputs.version }} + run: | + docker run --rm -e API_VERSION -v "$PWD/${{ inputs.schema-dir }}:/spec" redocly/cli:2.35.1@sha256:404c57d791f2a9d08973bb568da2e070bbc7bb96c94248eba38493537b78abb5 \ + bundle ${{ inputs.country }} --config redocly.yaml -o openapi.yaml + + # Decorators only run during `bundle`, not `lint` — linting the pre-bundle source wouldn't + # exercise the country-filtering transformation at all, so lint the bundled (now in-place) + # result instead. + - name: Lint the bundled ${{ inputs.country }} spec + shell: bash + run: | + docker run --rm -v "$PWD/${{ inputs.schema-dir }}:/spec" redocly/cli:2.35.1@sha256:404c57d791f2a9d08973bb568da2e070bbc7bb96c94248eba38493537b78abb5 lint openapi.yaml + + # target/classes/schemas/v1/openapi.yaml was already zipped into the jar by the caller's own + # `mvn package`. jar:jar just re-zips target/classes — no recompile, no test run — so the jar + # on disk picks up the file we just overwrote. + - name: Re-jar with the filtered spec + shell: bash + run: ./mvnw jar:jar diff --git a/composite-actions/fiscal/openapi-actions/decorators/openapi.js b/composite-actions/fiscal/openapi-actions/decorators/openapi.js new file mode 100644 index 00000000..e4069d7d --- /dev/null +++ b/composite-actions/fiscal/openapi-actions/decorators/openapi.js @@ -0,0 +1,104 @@ +// For any component schema with a `oneOf` whose branches are tagged `x-country` on the branch +// component itself, keeps only the branch matching the configured `country` and deletes now- +// unreferenced x-country-tagged components. Modeled on Redocly's built-in `remove-x-internal` +// decorator (walk components.schemas, delete tagged nodes). +function StripOtherCountries({ country }) { + if (!country) { + throw new Error('openapi/strip-other-countries requires a `country` option'); + } + return { + Root: { + leave(root) { + const schemas = root?.components?.schemas; + if (!schemas) return; + + for (const [schemaName, schema] of Object.entries(schemas)) { + if (!Array.isArray(schema?.oneOf)) continue; + + const before = schema.oneOf; + schema.oneOf = before.filter((branch) => { + const branchName = refName(branch?.$ref); + const branchCountry = branchName && schemas[branchName]?.['x-country']; + return !branchCountry || branchCountry === country; + }); + + for (const branch of before) { + if (!schema.oneOf.includes(branch)) { + console.log( + `openapi: removed '${refName(branch?.$ref)}' from '${schemaName}.oneOf' (x-country != ${country})` + ); + } + } + + if (schema.oneOf.length === 0) { + throw new Error( + `openapi: no branch of '${schemaName}' tagged x-country: ${country} — add ` + + `one before configuring redocly.yaml's '${country}' apis entry.` + ); + } + + // A single surviving branch makes the oneOf wrapper pointless — collapse the branch's + // content into the wrapper itself so consumers see one plain schema, not a oneOf of one. + if (schema.oneOf.length === 1) { + const branchName = refName(schema.oneOf[0].$ref); + const branchSchema = schemas[branchName]; + delete schema.oneOf; + Object.assign(schema, branchSchema); + delete schema['x-country']; + delete schema['title']; + console.log(`openapi: '${branchName}' is copied into '${schemaName}'`); + } + } + + for (const [schemaName, schema] of Object.entries(schemas)) { + if (schema?.['x-country'] && !isReferenced(schemas, schemaName)) { + delete schemas[schemaName]; + console.log(`openapi: dropped unreferenced component '${schemaName}'`); + } + } + } + } + }; +} + +function refName(ref) { + return typeof ref === 'string' && ref.startsWith('#/components/schemas/') + ? ref.slice('#/components/schemas/'.length) + : undefined; +} + +// ponytail: string-scan ref check, not a real ref-graph walk — fine while component names are +// unique tokens; swap for a proper walker if a name ever collides as a substring of another ref. +function isReferenced(schemas, name) { + return JSON.stringify(schemas).includes(`#/components/schemas/${name}`); +} + +// Overrides info.version from the API_VERSION env var when set, so each adapter stamps its +// own build version onto the shared engine spec as part of the same bundle pass — no pom filtering +// or text substitution. No-op when the env var is empty (the engine's literal ${project.version} +// token is left in place, which is fine for local/acceptance runs where the version is cosmetic). +function SetVersion() { + return { + Info: { + leave(info) { + const version = process.env.API_VERSION; + if (version) { + info.version = version; + console.log(`openapi: set info.version = ${version}`); + } + } + } + }; +} + +module.exports = function openapiPlugin() { + return { + id: 'openapi', + decorators: { + oas3: { + 'strip-other-countries': StripOtherCountries, + 'set-version': SetVersion + } + } + }; +}; diff --git a/composite-actions/fiscal/openapi-actions/redocly.yaml b/composite-actions/fiscal/openapi-actions/redocly.yaml new file mode 100644 index 00000000..1132f6ee --- /dev/null +++ b/composite-actions/fiscal/openapi-actions/redocly.yaml @@ -0,0 +1,12 @@ +extends: [recommended] + +plugins: ['decorators/openapi.js'] + +# Applies to every api below: stamps info.version from the API_VERSION env var when set. +decorators: + openapi/set-version: on + +apis: + belgium: + root: openapi.yaml + decorators: { openapi/strip-other-countries: { country: belgium } } diff --git a/composite-actions/fiscal/spanner-pgadapter-execute-sql/action.yaml b/composite-actions/fiscal/spanner-pgadapter-execute-sql/action.yaml new file mode 100644 index 00000000..25ddde23 --- /dev/null +++ b/composite-actions/fiscal/spanner-pgadapter-execute-sql/action.yaml @@ -0,0 +1,83 @@ +name: 'Spanner (PG-dialect) execute SQL' +description: >- + Execute caller-supplied SQL against a PostgreSQL-dialect Cloud Spanner database + by fronting it with PGAdapter and using the standard psql client. Domain-agnostic + (the SQL is an input). A sibling of spanner-pgadapter-liquibase for ad-hoc DML/DDL. + +inputs: + service-account-key: + description: 'GCP service-account key (JSON) with access to the Spanner database.' + required: true + project: + description: 'GCP project hosting the Spanner instance.' + required: true + instance: + description: 'Spanner instance id.' + required: false + default: 'fiscal-signing' + database: + description: 'Spanner database id (must be PostgreSQL dialect).' + required: true + sql: + description: 'SQL to execute. May contain multiple semicolon-separated statements.' + required: true + pgadapter-image: + description: >- + PGAdapter image. Pinned by digest for immutability (Dependabot's docker ecosystem only + scans Dockerfile/docker-compose.yml, not action.yaml inputs, so this is NOT auto-updated — + check https://console.cloud.google.com/artifacts/docker/cloud-spanner-pg-adapter/us/gcr.io/pgadapter + periodically and bump both the tag and digest together by hand. + required: false + default: 'gcr.io/cloud-spanner-pg-adapter/pgadapter:v0.55.0@sha256:30bb42ded681effccf83f93e5f5107c1ef9e5e48513d0f7e1d30c7bfc69fa5fa' + +runs: + using: composite + steps: + # Authenticate via the trusted action. It writes the credential file, registers it for + # log masking, and exports GOOGLE_APPLICATION_CREDENTIALS — so the key value is never + # interpolated into a shell command or handled by this action directly. + - uses: extenda/actions/setup-gcloud@v0 + with: + service-account-key: ${{ inputs.service-account-key }} + + - name: Start PGAdapter + shell: bash + run: | + if [ -z "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]; then + echo "GOOGLE_APPLICATION_CREDENTIALS not set; setup-gcloud must run first" >&2 + exit 1 + fi + # Mount the credential FILE (a path, not the secret value) and let PGAdapter pick it + # up via Application Default Credentials. + docker run -d --name pgadapter \ + -p 5432:5432 \ + -v "${GOOGLE_APPLICATION_CREDENTIALS}:/var/run/secrets/gcp-credentials.json:ro" \ + -e GOOGLE_APPLICATION_CREDENTIALS=/var/run/secrets/gcp-credentials.json \ + "${{ inputs.pgadapter-image }}" \ + -p "${{ inputs.project }}" -i "${{ inputs.instance }}" -x + # PGAdapter is the last thing to open the port; wait until it accepts TCP. + for i in $(seq 1 30); do + if (exec 3<>/dev/tcp/localhost/5432) 2>/dev/null; then echo "PGAdapter ready"; exit 0; fi + sleep 1 + done + echo "PGAdapter did not become ready" >&2 + docker logs pgadapter || true + exit 1 + + - name: Execute SQL + shell: bash + # SQL is passed via the environment and piped to psql's stdin, so its contents are + # never interpolated into the command line. PGAdapter authenticates to Spanner via the + # mounted key; the client-side username/password are unused. + env: + SQL: ${{ inputs.sql }} + DATABASE: ${{ inputs.database }} + run: | + printf '%s\n' "${SQL}" | psql \ + "postgresql://spanner:spanner@localhost:5432/${DATABASE}?sslmode=disable" \ + -v ON_ERROR_STOP=1 + + - name: Stop PGAdapter + if: always() + shell: bash + run: docker rm -f pgadapter || true diff --git a/composite-actions/fiscal/spanner-pgadapter-liquibase/README.md b/composite-actions/fiscal/spanner-pgadapter-liquibase/README.md new file mode 100644 index 00000000..fd2ed5fb --- /dev/null +++ b/composite-actions/fiscal/spanner-pgadapter-liquibase/README.md @@ -0,0 +1,39 @@ +# spanner-pgadapter-liquibase + +Runs **standard Postgres Liquibase** against a **PostgreSQL-dialect Cloud Spanner** +database by fronting Spanner with [PGAdapter](https://github.com/GoogleCloudPlatform/pgadapter). + +It is the PG-dialect counterpart to `extenda/shared-workflows/generic/cloud-sql-liquibase` +(which targets Cloud SQL via the Cloud SQL Auth Proxy) and a deliberate alternative to +`extenda/actions/liquibase-spanner` (which speaks Spanner's **GoogleSQL** dialect). The fiscal +engine standardised on one portable Postgres SQL codepath, so its changesets are applied with +plain Postgres Liquibase here too — see engine decisions #9/#10. + +This is a shared composite action, consumed as +`extenda/shared-workflows/composite-actions/fiscal/spanner-pgadapter-liquibase@v0`. + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `service-account-key` | yes | – | GCP SA key (JSON) with access to the Spanner database. | +| `project` | yes | – | GCP project hosting the Spanner instance. | +| `instance` | no | `fiscal-signing` | Spanner instance id. | +| `database` | yes | – | Spanner database id (**must** be PostgreSQL dialect). | +| `search-path` | yes | – | Directory containing the changelog and its includes. | +| `changelog-file` | no | `changelog-master.yaml` | Changelog file, relative to `search-path`. | +| `pgadapter-image` | no | pinned in `action.yaml` | PGAdapter image (tag+digest). | + +## Notes / validation status + +- **Credentials** are handled by `extenda/actions/setup-gcloud@v0` (same as + `cloud-sql-liquibase`): the action exports `GOOGLE_APPLICATION_CREDENTIALS`, and only that + file *path* is mounted into the PGAdapter container, which then uses Application Default + Credentials. The key value is never interpolated into a shell command or written by hand. +- Sets `spanner.ddl_transaction_mode=AutocommitExplicitTransaction` on the connection so + PGAdapter converts Liquibase's transactional DDL into Spanner DDL batches. +- The fiscal changesets ship on the `fiscal-engine` jar, so callers extract them first + (e.g. `mvn dependency:unpack-dependencies`) and pass the directory as `search-path`. +- **Not yet validated in CI against a real Spanner instance.** The PGAdapter flags and the + `liquibase-github-actions/update` input names should be confirmed on a first run before + relying on it for prod. diff --git a/composite-actions/fiscal/spanner-pgadapter-liquibase/action.yaml b/composite-actions/fiscal/spanner-pgadapter-liquibase/action.yaml new file mode 100644 index 00000000..9b496ea7 --- /dev/null +++ b/composite-actions/fiscal/spanner-pgadapter-liquibase/action.yaml @@ -0,0 +1,88 @@ +name: 'Spanner (PG-dialect) Liquibase migration' +description: >- + Run standard Postgres Liquibase against a PostgreSQL-dialect Cloud Spanner + database by fronting it with PGAdapter. The PG-dialect counterpart to + generic/cloud-sql-liquibase (which targets Cloud SQL) and a deliberate + alternative to extenda/actions/liquibase-spanner (which is GoogleSQL dialect). + +inputs: + service-account-key: + description: 'GCP service-account key (JSON) with access to the Spanner database.' + required: true + project: + description: 'GCP project hosting the Spanner instance.' + required: true + instance: + description: 'Spanner instance id.' + required: false + default: 'fiscal-signing' + database: + description: 'Spanner database id (must be PostgreSQL dialect).' + required: true + changelog-file: + description: 'Liquibase changelog file, relative to search-path.' + required: false + default: 'changelog-master.yaml' + search-path: + description: 'Directory that contains the changelog (and its includes).' + required: true + pgadapter-image: + description: >- + PGAdapter image. Pinned by digest for immutability (Dependabot's docker ecosystem only + scans Dockerfile/docker-compose.yml, not action.yaml inputs, so this is NOT auto-updated — + check https://console.cloud.google.com/artifacts/docker/cloud-spanner-pg-adapter/us/gcr.io/pgadapter + periodically and bump both the tag and digest together by hand. + required: false + default: 'gcr.io/cloud-spanner-pg-adapter/pgadapter:v0.55.0@sha256:30bb42ded681effccf83f93e5f5107c1ef9e5e48513d0f7e1d30c7bfc69fa5fa' + +runs: + using: composite + steps: + # Authenticate via the trusted action. It writes the credential file, registers it for + # log masking, and exports GOOGLE_APPLICATION_CREDENTIALS — so the key value is never + # interpolated into a shell command or handled by this action directly. + - uses: extenda/actions/setup-gcloud@v0 + with: + service-account-key: ${{ inputs.service-account-key }} + + - name: Start PGAdapter + shell: bash + run: | + if [ -z "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]; then + echo "GOOGLE_APPLICATION_CREDENTIALS not set; setup-gcloud must run first" >&2 + exit 1 + fi + # Mount the credential FILE (a path, not the secret value) and let PGAdapter pick it + # up via Application Default Credentials. + docker run -d --name pgadapter \ + -p 5432:5432 \ + -v "${GOOGLE_APPLICATION_CREDENTIALS}:/var/run/secrets/gcp-credentials.json:ro" \ + -e GOOGLE_APPLICATION_CREDENTIALS=/var/run/secrets/gcp-credentials.json \ + "${{ inputs.pgadapter-image }}" \ + -p "${{ inputs.project }}" -i "${{ inputs.instance }}" -x + # PGAdapter is the last thing to open the port; wait until it accepts TCP. + for i in $(seq 1 30); do + if (exec 3<>/dev/tcp/localhost/5432) 2>/dev/null; then echo "PGAdapter ready"; exit 0; fi + sleep 1 + done + echo "PGAdapter did not become ready" >&2 + docker logs pgadapter || true + exit 1 + + # ddl_transaction_mode=AutocommitExplicitTransaction makes PGAdapter turn the + # transactional DDL Liquibase emits into Spanner DDL batches (Spanner forbids + # DDL inside explicit transactions). PGAdapter authenticates to Spanner via the + # mounted key, so the client-side username/password are unused. + - name: Run Liquibase update + uses: liquibase-github-actions/update@db87f3cf24f450dd01a6784a4cd7feec97a21676 # v4.25.1 + with: + changelogFile: ${{ inputs.changelog-file }} + searchPath: ${{ inputs.search-path }} + url: 'jdbc:postgresql://localhost:5432/${{ inputs.database }}?options=-c%20spanner.ddl_transaction_mode=AutocommitExplicitTransaction' + username: spanner + password: spanner + + - name: Stop PGAdapter + if: always() + shell: bash + run: docker rm -f pgadapter || true