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
58 changes: 31 additions & 27 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,59 +1,63 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
ast (2.4.3)
drb (2.2.3)
json (2.7.2)
language_server-protocol (3.17.0.3)
json (2.19.7)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
minitest (6.0.2)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
mutex_m (0.3.0)
parallel (1.24.0)
parser (3.3.1.0)
parallel (1.28.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
prism (1.9.0)
racc (1.7.3)
racc (1.8.1)
rainbow (3.1.1)
rake (13.3.1)
regexp_parser (2.9.1)
rexml (3.4.2)
rubocop (1.63.5)
rake (13.4.2)
regexp_parser (2.12.0)
rubocop (1.84.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3)
parser (>= 3.3.1.0)
rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (1.13.0)
standard (1.36.0)
standard (1.54.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.63.0)
rubocop (~> 1.84.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.4)
standard-performance (~> 1.8)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.4.0)
standard-performance (1.9.0)
lint_roller (~> 1.1)
rubocop-performance (~> 1.21.0)
unicode-display_width (2.5.0)
rubocop-performance (~> 1.26.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)

PLATFORMS
ruby
x86_64-darwin-23
x86_64-linux

DEPENDENCIES
minitest
Expand Down
60 changes: 46 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,60 @@ It validates the .env file to ensure that we actually have defined a secret for
1. ENVIRONMENT_NAME_SECRET_KEY (e.g. STAGING_SECRET_KEY will replace SECRET_KEY in .env.staging)
2. SECRET_KEY (e.g. SECRET_KEY will replace SECRET_KEY in .env.staging only if STAGING_SECRET_KEY is not defined)

## Inputs

| Input | Required | Default | Description |
|---|---|---|---|
| `secrets` | Yes | — | JSON glob of all secrets (`${{ toJSON(secrets) }}`). |
| `environment-name` | Yes | — | The environment to replace variables for (e.g. `staging`). |
| `env-file-path` | Yes | — | Path to the output file. |
| `template-file-path` | No | — | Explicit path to the template file. Use this when the template does not follow the `<env-file-path>.<environment-name>` naming convention (e.g. `appsettings.Production.json`). When omitted, the template is inferred from `env-file-path` + `environment-name`. |
| `delete-template` | No | `true` | Whether to delete the template file after writing the output. Set to `false` to keep it. |
Comment thread
sean-dickinson marked this conversation as resolved.
| `additional-variables` | No | `{}` | JSON object of extra non-secret variables to substitute (e.g. `{"APP_SHA": "abc123"}`). |

## Usage

The following is an example of how to use this action in your github workflow.
**Convention mode** — template is inferred from `env-file-path` + `environment-name`:
```yaml
name: Replace Environment Secrets
uses: bythepixel/env-replacer-action@1.0.0
with:
- name: Replace Environment Secrets
uses: bythepixel/env-replacer-action@1.0.0
with:
environment-name: staging
env-file-path: .env
secrets: ${{ toJSON(secrets) }}
```

If you have additional variables you would like to include that are not secrets but are dynamic, you can pass them in as well using the additional-variables input.
**Explicit template mode** — use `template-file-path` when the template doesn't follow the standard naming convention:
```yaml
- name: Replace Environment Secrets
uses: bythepixel/env-replacer-action@1.0.0
with:
environment-name: production
template-file-path: appsettings.Production.json
env-file-path: appsettings.json
secrets: ${{ toJSON(secrets) }}
```

If you have additional variables that are not secrets but are dynamic, pass them via `additional-variables`:
```yaml
name: Replace Environment Secrets
- name: Replace Environment Secrets
uses: bythepixel/env-replacer-action@1.0.0
with:
environment-name: staging
env-file-path: .env
secrets: ${{ toJSON(secrets) }}
additional-variables: '{"APP_SHA": "${{ env.sha }}" }'
- name: Replace Environment Secrets
uses: bythepixel/env-replacer-action@1.0.0
with:
environment-name: staging
env-file-path: .env
secrets: ${{ toJSON(secrets) }}
additional-variables: '{"APP_SHA": "${{ env.sha }}" }'
```

To keep the template file after replacement (e.g. for debugging), set `delete-template: false`:
```yaml
- name: Replace Environment Secrets
uses: bythepixel/env-replacer-action@1.0.0
with:
environment-name: staging
env-file-path: .env
secrets: ${{ toJSON(secrets) }}
delete-template: false
```

## Examples
Expand All @@ -42,7 +74,7 @@ You can cross reference the [examples](./examples) directory as well as the defi
- This action is written as a "composite" action, meaning it runs on github runner that uses it.
- It does not use docker or any other dependencies. It is written in Ruby with no gem dependencies. Github runners come with Ruby pre-installed and we are not using any version specific features.
- The moment you need to use a gem, you will need to update the action to install a specific ruby version and bundle install the gems.
- This action will take the input file, replace all the keys with the secrets you pass in, and write to the file you specify. It will delete the original "environment specific" version of the file.
- This action will take the template file, replace all the keys with the secrets you pass in, and write to the output file you specify. By default it deletes the template file after writing; set `delete-template: false` to keep it.

# Local Development

Expand Down
17 changes: 13 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ inputs:
description: 'The environment to replace variables for'
required: true
env-file-path:
description: 'The path to the final environment file to generate. It should have a sibling file with the same name but with a .<environment-name> extension. Ex. .env.staging'
description: 'Path to the final environment file to generate. This may be the same as the template file path.'
required: true
template-file-path:
description: 'Optional explicit path to the template file. Overrides the <env-file-path>.<environment-name> convention. Use for files that do not follow that naming, e.g. appsettings.Production.json.'
required: false
delete-template:
description: 'Whether to delete the template file after writing the output. Defaults to true so that e.g. .env.production is not left alongside .env.'
required: false
default: 'true'
additional-variables:
description: 'A json glob of additional variables to use in the replacement'
required: false
Expand All @@ -22,7 +29,7 @@ runs:
shell: bash
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
to_envs() { jq -r "to_entries[] | \"\(.key)<<$EOF\n\(.value)\n$EOF\n\""; }
to_envs() { jq -r "to_entries[] | \"\(.key)<<$EOF\n\(.value)\n$EOF\n\""; }
echo "$SECRETS_CONTEXT" | to_envs >> $GITHUB_ENV
env:
SECRETS_CONTEXT: ${{ inputs.secrets }}
Expand All @@ -31,15 +38,17 @@ runs:
shell: bash
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
to_envs() { jq -r "to_entries[] | \"\(.key)<<$EOF\n\(.value)\n$EOF\n\""; }
to_envs() { jq -r "to_entries[] | \"\(.key)<<$EOF\n\(.value)\n$EOF\n\""; }
echo "$ADDITIONAL_VARIABLES" | to_envs >> $GITHUB_ENV
env:
ADDITIONAL_VARIABLES: ${{ inputs.additional-variables }}

- name: Run replacement
shell: bash
run: |
${GITHUB_ACTION_PATH}/bin/replace $ENV_FILE_PATH $ENVIRONMENT_NAME
"${GITHUB_ACTION_PATH}/bin/replace"
env:
ENV_FILE_PATH: ${{ inputs.env-file-path }}
ENVIRONMENT_NAME: ${{ inputs.environment-name }}
TEMPLATE_FILE_PATH: ${{ inputs.template-file-path }}
DELETE_TEMPLATE: ${{ inputs.delete-template }}
17 changes: 12 additions & 5 deletions bin/replace
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
#!/usr/bin/env ruby
require_relative '../lib/replacer'
require "optparse"
require_relative "../lib/replacer"

# Example usage:
# ruby replacer.rb .env staging
# Note that we are expecting to find a .env.staging file for this example and will end up creating a new file with the replaced tokens called .env
def coerce_empty_string_to_nil(value) = value.empty? ? nil : value

Replacer.from_args(ARGV).replace
options = {
environment: ENV.fetch("ENVIRONMENT_NAME"),
output_file: ENV.fetch("ENV_FILE_PATH"),
template_path: coerce_empty_string_to_nil(ENV["TEMPLATE_FILE_PATH"]),
delete_template: ENV.fetch("DELETE_TEMPLATE", "true") == "true"
}


Replacer.from(**options).replace
32 changes: 14 additions & 18 deletions lib/replacer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ class Replacer
class MissingTokensError < StandardError; end

class << self
# Factory to create a new Replacer instance from positional command line arguments
def from_args(args)
validate_args!(args)
environment = args[1]
new(file_path(args), environment)
def from(environment:, output_file:, template_path: nil, delete_template: true)
template = template_path || "#{output_file}.#{environment}"
fail_unless_file!(template)
new(template, environment, output_file, delete_template: delete_template)
Comment thread
sean-dickinson marked this conversation as resolved.
end

private
Expand All @@ -22,38 +21,35 @@ def file_path(args)
args.join(".")
end

def validate_args!(args)
raise ArgumentError, "Usage: ruby replacer.rb <file_path> <environment>" if args.length != 2
raise ArgumentError, "File not found: #{File.expand_path(file_path(args))}" unless File.exist?(file_path(args))
def fail_unless_file!(file_path)
raise ArgumentError, "File not found: #{File.expand_path(file_path)}" unless File.exist?(file_path)
end
end

attr_reader :normalized_environment

def initialize(file_path, environment)
@file_path = file_path
def initialize(template_path, environment, output_path, delete_template: true)
@template_path = template_path
@environment = environment
@output_path = output_path
@delete_template = delete_template
@normalized_environment = environment.upcase.tr("-", "_")
validate!
end

def replace
content = File.read(@file_path)
content = File.read(@template_path)
tokens_needing_replacement.each do |token|
content.gsub!(/(?<!\$)\{#{token}\}/, get_value(token))
end
File.write(final_file_path, content)
File.delete(@file_path)
File.write(@output_path, content)
File.delete(@template_path) if @delete_template && @output_path != @template_path
end

private

def final_file_path
@file_path.gsub(".#{@environment}", "")
end

def tokens_needing_replacement
@tokens_needing_replacement ||= File.read(@file_path)
@tokens_needing_replacement ||= File.read(@template_path)
.scan(/(?<!\$)\{(\w+)\}/).flatten
end

Expand Down
Loading
Loading