Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ A GitHub Action for deploying applications to [Gigalixir](https://gigalixir.com)
| `deploy_timeout` | No | `0` | Max seconds to wait for deployment rollout to complete (0 = skip) |
| `replicas` | No | | Number of replicas to run (used with `action: scale`) |
| `size` | No | | Size of each replica between 0.5 and 128 (used with `action: scale`) |
| `config_*` | No | | Config variables to set before deploy (see below) |
| `configs` | No | | Config variables to set before deploy as multiline `KEY=VALUE` pairs (see below) |

## Outputs

Expand Down Expand Up @@ -219,17 +219,18 @@ Deploy an app that lives in a subdirectory using `git subtree push`:

### Set Config Variables Before Deploy

Any input prefixed with `config_` will be set as a Gigalixir environment variable (with the prefix stripped) before deploying. Config is applied with `avoid_restart=true` since the deploy handles the restart.
Use the `configs` input to set Gigalixir environment variables before deploying. Provide one `KEY=VALUE` pair per line. Config is applied with `avoid_restart=true` since the deploy handles the restart.

```yaml
- uses: gigalixir/gigalixir-action@v1
with:
gigalixir_email: ${{ secrets.GIGALIXIR_EMAIL }}
gigalixir_api_key: ${{ secrets.GIGALIXIR_API_KEY }}
app_name: my-app
config_MIX_ENV: prod
config_SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}
config_DATABASE_POOL_SIZE: "10"
configs: |
MIX_ENV=prod
SECRET_KEY_BASE=${{ secrets.SECRET_KEY_BASE }}
DATABASE_POOL_SIZE=10
```

### Wait for Deployment Rollout
Expand Down
163 changes: 163 additions & 0 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,169 @@ describe('Gigalixir Deploy Action', () => {
})
})

describe('configs input', () => {
function mockHttpsResponse(statusCode: number, body: string): void {
const https = require('https')
https.request.mockImplementation(
(
_options: unknown,
callback: (res: {
statusCode: number
on: (event: string, cb: (data?: string) => void) => void
}) => void
) => {
const res = {
statusCode,
on: jest.fn((event: string, cb: (data?: string) => void) => {
if (event === 'data') cb(body)
if (event === 'end') cb()
})
}
callback(res)
return {
on: jest.fn(),
write: jest.fn(),
end: jest.fn()
}
}
)
}

it('should set config from multiline KEY=VALUE input', async () => {
mockedCore.getInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
gigalixir_email: 'test@example.com',
gigalixir_api_key: 'test-api-key',
app_name: 'test-app',
action: 'deploy',
configs: 'FOO=bar\nBAZ=qux',
github_deployments: 'false'
}
return inputs[name] || ''
})
mockHttpsResponse(201, '{}')

await runAction()

const https = require('https')
const writeCall = https.request.mock.results[0].value.write
expect(writeCall).toHaveBeenCalledWith(
JSON.stringify({
configs: { FOO: 'bar', BAZ: 'qux' },
avoid_restart: true
})
)
})

it('should handle values containing equals signs', async () => {
mockedCore.getInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
gigalixir_email: 'test@example.com',
gigalixir_api_key: 'test-api-key',
app_name: 'test-app',
action: 'deploy',
configs: 'DATABASE_URL=postgres://u:p@host/db?opt=1',
github_deployments: 'false'
}
return inputs[name] || ''
})
mockHttpsResponse(201, '{}')

await runAction()

const https = require('https')
const writeCall = https.request.mock.results[0].value.write
expect(writeCall).toHaveBeenCalledWith(
JSON.stringify({
configs: { DATABASE_URL: 'postgres://u:p@host/db?opt=1' },
avoid_restart: true
})
)
})

it('should skip blank lines and comments', async () => {
mockedCore.getInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
gigalixir_email: 'test@example.com',
gigalixir_api_key: 'test-api-key',
app_name: 'test-app',
action: 'deploy',
configs: '# a comment\nFOO=bar\n\nBAZ=qux\n',
github_deployments: 'false'
}
return inputs[name] || ''
})
mockHttpsResponse(201, '{}')

await runAction()

const https = require('https')
const writeCall = https.request.mock.results[0].value.write
expect(writeCall).toHaveBeenCalledWith(
JSON.stringify({
configs: { FOO: 'bar', BAZ: 'qux' },
avoid_restart: true
})
)
})

it('should warn on invalid lines without equals sign', async () => {
mockedCore.getInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
gigalixir_email: 'test@example.com',
gigalixir_api_key: 'test-api-key',
app_name: 'test-app',
action: 'deploy',
configs: 'INVALID_LINE\nFOO=bar',
github_deployments: 'false'
}
return inputs[name] || ''
})
mockHttpsResponse(201, '{}')

await runAction()

expect(mockedCore.warning).toHaveBeenCalledWith(
expect.stringContaining("no '=' found")
)
const https = require('https')
const writeCall = https.request.mock.results[0].value.write
expect(writeCall).toHaveBeenCalledWith(
JSON.stringify({
configs: { FOO: 'bar' },
avoid_restart: true
})
)
})

it('should merge configs input with legacy config_ prefix', async () => {
process.env.INPUT_CONFIG_LEGACY = 'old_way'
mockedCore.getInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
gigalixir_email: 'test@example.com',
gigalixir_api_key: 'test-api-key',
app_name: 'test-app',
action: 'deploy',
configs: 'NEW_WAY=yes',
github_deployments: 'false'
}
return inputs[name] || ''
})
mockHttpsResponse(201, '{}')

await runAction()

const https = require('https')
const writeCall = https.request.mock.results[0].value.write
expect(writeCall).toHaveBeenCalledWith(
JSON.stringify({
configs: { LEGACY: 'old_way', NEW_WAY: 'yes' },
avoid_restart: true
})
)
})
})

describe('deployment verification', () => {
// Helper to set up https mock for sequential API responses
function mockHttpsResponses(
Expand Down
8 changes: 7 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,16 @@ inputs:
required: false
default: ''
deprecationMessage: 'SSH_PRIVATE_KEY is not supported. Use gigalixir/gigalixir-action@v0 for SSH-based migration support.'
# Config variables can be set using the config_ prefix:
configs:
description: 'Config variables to set before deploy, as multiline KEY=VALUE pairs'
required: false
default: ''
# Config variables can also be set using the config_ prefix:
# config_MY_VAR: my-value
# Any input starting with config_ will be set as a Gigalixir config
# variable (with the config_ prefix stripped) before deploying.
# NOTE: The config_ prefix approach triggers GitHub Actions warnings
# about unexpected inputs. Use the "configs" input instead.

outputs:
deploy_status:
Expand Down
25 changes: 24 additions & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25753,6 +25753,7 @@ async function run() {
break;
case 'create':
await handleCreate(email, apiKey, appName);
await setAppConfig(email, apiKey, appName);
break;
case 'destroy':
await handleDestroy(email, apiKey, appName);
Expand Down Expand Up @@ -25801,14 +25802,36 @@ async function run() {
}
}
async function setAppConfig(email, apiKey, appName) {
const prefix = 'INPUT_CONFIG_';
const configs = {};
// Legacy: config_* prefix inputs (triggers GitHub Actions warnings)
const prefix = 'INPUT_CONFIG_';
for (const [envKey, envValue] of Object.entries(process.env)) {
if (envKey.startsWith(prefix) && envValue !== undefined) {
const configKey = envKey.substring(prefix.length);
configs[configKey] = envValue;
}
}
// New: configs input (multiline KEY=VALUE pairs)
const configsInput = core.getInput('configs');
if (configsInput) {
for (const line of configsInput.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#'))
continue;
const eqIndex = trimmed.indexOf('=');
if (eqIndex === -1) {
core.warning(`Ignoring invalid config line (no '=' found): ${trimmed}`);
continue;
}
const key = trimmed.substring(0, eqIndex).trim();
const value = trimmed.substring(eqIndex + 1).trim();
if (!key) {
core.warning(`Ignoring config line with empty key: ${trimmed}`);
continue;
}
configs[key] = value;
}
}
if (Object.keys(configs).length === 0) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/main.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 24 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export async function run(): Promise<void> {
break
case 'create':
await handleCreate(email, apiKey, appName)
await setAppConfig(email, apiKey, appName)
break
case 'destroy':
await handleDestroy(email, apiKey, appName)
Expand Down Expand Up @@ -171,16 +172,38 @@ export async function setAppConfig(
apiKey: string,
appName: string
): Promise<void> {
const prefix = 'INPUT_CONFIG_'
const configs: Record<string, string> = {}

// Legacy: config_* prefix inputs (triggers GitHub Actions warnings)
const prefix = 'INPUT_CONFIG_'
for (const [envKey, envValue] of Object.entries(process.env)) {
if (envKey.startsWith(prefix) && envValue !== undefined) {
const configKey = envKey.substring(prefix.length)
configs[configKey] = envValue
}
}

// New: configs input (multiline KEY=VALUE pairs)
const configsInput = core.getInput('configs')
if (configsInput) {
for (const line of configsInput.split('\n')) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) continue
const eqIndex = trimmed.indexOf('=')
if (eqIndex === -1) {
core.warning(`Ignoring invalid config line (no '=' found): ${trimmed}`)
continue
}
const key = trimmed.substring(0, eqIndex).trim()
const value = trimmed.substring(eqIndex + 1).trim()
if (!key) {
core.warning(`Ignoring config line with empty key: ${trimmed}`)
continue
}
configs[key] = value
}
}

if (Object.keys(configs).length === 0) {
return
}
Expand Down