settings_reader-vault_resolver 0.4.9 → 0.5.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/linters.yml +21 -11
- data/.github/workflows/main.yml +70 -5
- data/Gemfile.lock +2 -2
- data/docker-compose.yml +22 -1
- data/lib/settings_reader/vault_resolver/cache.rb +2 -2
- data/lib/settings_reader/vault_resolver/configuration.rb +6 -0
- data/lib/settings_reader/vault_resolver/engines/abstract.rb +3 -3
- data/lib/settings_reader/vault_resolver/engines/aws.rb +29 -0
- data/lib/settings_reader/vault_resolver/entry.rb +5 -1
- data/lib/settings_reader/vault_resolver/refresher.rb +1 -1
- data/lib/settings_reader/vault_resolver/version.rb +1 -1
- data/lib/settings_reader/vault_resolver.rb +1 -0
- metadata +4 -4
- data/.circleci/config.yml +0 -114
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3e683be3539c3e3fd46bf0de60c78f11c7aeda4bbc321246298ec19b068fa84
|
4
|
+
data.tar.gz: 80a0bae40ee7bef3f22ac77761685fa89a53eeb21311295590dea2378c2b0a9b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c957e71f2730a18abe2b8a21e10d4fd93f1f0efcf779d7ca1f4cca791363a5bc5a2daeee83510b9a03b71045c7b8700957ce571ca5462355976e2fd203bd71a
|
7
|
+
data.tar.gz: ce4d399a56ec38e4bf39f87ecb759a0f5a77b7269f84de42a8d32b0ef477e25b38522513c03bcd92f60a47eeb43867a69af797a751f1e108ab071fb0f0a6cebf
|
@@ -9,6 +9,19 @@ on:
|
|
9
9
|
- cron: '30 0 * * 1'
|
10
10
|
|
11
11
|
jobs:
|
12
|
+
rubocop:
|
13
|
+
runs-on: ubuntu-latest
|
14
|
+
|
15
|
+
steps:
|
16
|
+
- name: Checkout
|
17
|
+
uses: actions/checkout@v3
|
18
|
+
- name: Set up Ruby
|
19
|
+
uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: 2.5
|
22
|
+
bundler-cache: true
|
23
|
+
- name: Run rubocop
|
24
|
+
run: bundle exec rubocop --parallel
|
12
25
|
|
13
26
|
code-ql:
|
14
27
|
name: Analyze
|
@@ -24,16 +37,13 @@ jobs:
|
|
24
37
|
language: [ 'ruby' ]
|
25
38
|
|
26
39
|
steps:
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
- name: Initialize CodeQL
|
31
|
-
uses: github/codeql-action/init@v1
|
32
|
-
with:
|
33
|
-
languages: ${{ matrix.language }}
|
40
|
+
- name: Checkout repository
|
41
|
+
uses: actions/checkout@v3
|
34
42
|
|
35
|
-
|
36
|
-
|
43
|
+
- name: Initialize CodeQL
|
44
|
+
uses: github/codeql-action/init@v2
|
45
|
+
with:
|
46
|
+
languages: ${{ matrix.language }}
|
37
47
|
|
38
|
-
|
39
|
-
|
48
|
+
- name: Perform CodeQL Analysis
|
49
|
+
uses: github/codeql-action/analyze@v2
|
data/.github/workflows/main.yml
CHANGED
@@ -10,6 +10,72 @@ on:
|
|
10
10
|
types: [published]
|
11
11
|
|
12
12
|
jobs:
|
13
|
+
build:
|
14
|
+
env:
|
15
|
+
VAULT_ADDR: 'http://127.0.0.1:8200'
|
16
|
+
VAULT_TOKEN: 'vault_root_token'
|
17
|
+
DATABASE_ADDR: 'database'
|
18
|
+
runs-on: ubuntu-latest
|
19
|
+
strategy:
|
20
|
+
matrix:
|
21
|
+
ruby: [ '2.7', '3.0', '3.3' ]
|
22
|
+
# services:
|
23
|
+
# vault:
|
24
|
+
# image: hashicorp/vault
|
25
|
+
# ports:
|
26
|
+
# - "8200:8200"
|
27
|
+
# env:
|
28
|
+
# VAULT_DEV_ROOT_TOKEN_ID: vault_root_token
|
29
|
+
# SKIP_SETCAP: true
|
30
|
+
# database:
|
31
|
+
# image: postgres:14.1-alpine
|
32
|
+
# ports:
|
33
|
+
# - "5432:5432"
|
34
|
+
# env:
|
35
|
+
# POSTGRES_USER: 'vault_root'
|
36
|
+
# POSTGRES_PASSWORD: 'root_password'
|
37
|
+
# POSTGRES_DB: 'app_db'
|
38
|
+
# options: >-
|
39
|
+
# --health-cmd pg_isready
|
40
|
+
# --health-interval 10s
|
41
|
+
# --health-timeout 5s
|
42
|
+
# --health-retries 5
|
43
|
+
# aws:
|
44
|
+
# image: localstack/localstack
|
45
|
+
# ports:
|
46
|
+
# - "127.0.0.1:4566:4566" # LocalStack Gateway
|
47
|
+
# - "127.0.0.1:4510-4559:4510-4559" # external services port range
|
48
|
+
# env:
|
49
|
+
# DEBUG: 0
|
50
|
+
# volumes:
|
51
|
+
# - '/home/runner/work/settings_reader-vault_resolver/local/localstack/app-access-policy.json:/etc/localstack/init/ready.d/app-access-policy.json'
|
52
|
+
# - '/home/runner/work/settings_reader-vault_resolver/local/localstack/app-assume-policy.json:/etc/localstack/init/ready.d/app-assume-policy.json'
|
53
|
+
# - '/home/runner/work/settings_reader-vault_resolver/local/localstack/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh'
|
54
|
+
steps:
|
55
|
+
- name: Checkout
|
56
|
+
uses: actions/checkout@v1
|
57
|
+
|
58
|
+
- name: Set up Ruby
|
59
|
+
uses: ruby/setup-ruby@v1
|
60
|
+
with:
|
61
|
+
ruby-version: ${{ matrix.ruby }}
|
62
|
+
bundler-cache: true
|
63
|
+
|
64
|
+
- name: Start Dependencies
|
65
|
+
run: |
|
66
|
+
docker-compose up -d
|
67
|
+
echo "Waiting 15 seconds for initial configuraiton"
|
68
|
+
sleep 15
|
69
|
+
|
70
|
+
- name: Run specs
|
71
|
+
env:
|
72
|
+
COVERAGE: true
|
73
|
+
run: bundle exec rspec
|
74
|
+
|
75
|
+
- name: Upload coverage
|
76
|
+
env:
|
77
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
78
|
+
run: bash <(curl -s https://codecov.io/bash)
|
13
79
|
|
14
80
|
release:
|
15
81
|
runs-on: ubuntu-latest
|
@@ -17,14 +83,13 @@ jobs:
|
|
17
83
|
if: github.event_name == 'release' && github.event.action == 'published'
|
18
84
|
steps:
|
19
85
|
- name: Checkout
|
20
|
-
uses: actions/checkout@
|
86
|
+
uses: actions/checkout@v3
|
21
87
|
|
22
88
|
- name: Set up Ruby
|
23
|
-
uses:
|
89
|
+
uses: ruby/setup-ruby@v1
|
24
90
|
with:
|
25
|
-
ruby-version: 2.7
|
26
|
-
|
27
|
-
run: gem install bundler:2.1.4
|
91
|
+
ruby-version: 2.7
|
92
|
+
bundler-cache: true
|
28
93
|
- name: Set up credentials
|
29
94
|
run: |
|
30
95
|
mkdir -p $HOME/.gem
|
data/Gemfile.lock
CHANGED
@@ -58,7 +58,7 @@ GEM
|
|
58
58
|
simplecov-html (~> 0.11)
|
59
59
|
simplecov_json_formatter (~> 0.1)
|
60
60
|
simplecov-html (0.12.3)
|
61
|
-
simplecov_json_formatter (0.1.
|
61
|
+
simplecov_json_formatter (0.1.4)
|
62
62
|
timecop (0.9.4)
|
63
63
|
unicode-display_width (2.1.0)
|
64
64
|
vault (0.16.0)
|
@@ -78,4 +78,4 @@ DEPENDENCIES
|
|
78
78
|
timecop
|
79
79
|
|
80
80
|
BUNDLED WITH
|
81
|
-
2.
|
81
|
+
2.2.32
|
data/docker-compose.yml
CHANGED
@@ -2,12 +2,20 @@
|
|
2
2
|
version: '3'
|
3
3
|
services:
|
4
4
|
vault:
|
5
|
-
image: vault
|
5
|
+
image: hashicorp/vault
|
6
6
|
ports:
|
7
7
|
- "8200:8200"
|
8
8
|
environment:
|
9
9
|
VAULT_DEV_ROOT_TOKEN_ID: 'vault_root_token'
|
10
10
|
SKIP_SETCAP: 'true'
|
11
|
+
# playground:
|
12
|
+
# image: hashicorp/vault
|
13
|
+
# command:
|
14
|
+
# - sleep
|
15
|
+
# - '10000000000'
|
16
|
+
# environment:
|
17
|
+
# VAULT_ADDR: 'http://aws:8200'
|
18
|
+
# VAULT_TOKEN: 'vault_root_token'
|
11
19
|
db:
|
12
20
|
image: postgres:14.1-alpine
|
13
21
|
ports:
|
@@ -16,11 +24,24 @@ services:
|
|
16
24
|
POSTGRES_USER: 'vault_root'
|
17
25
|
POSTGRES_PASSWORD: 'root_password'
|
18
26
|
POSTGRES_DB: 'app_db'
|
27
|
+
aws:
|
28
|
+
image: localstack/localstack
|
29
|
+
ports:
|
30
|
+
- "127.0.0.1:4566:4566" # LocalStack Gateway
|
31
|
+
- "127.0.0.1:4510-4559:4510-4559" # external services port range
|
32
|
+
environment:
|
33
|
+
# LocalStack configuration: https://docs.localstack.cloud/references/configuration/
|
34
|
+
- DEBUG=${DEBUG:-0}
|
35
|
+
volumes:
|
36
|
+
- './local/localstack/app-access-policy.json:/etc/localstack/init/ready.d/app-access-policy.json'
|
37
|
+
- './local/localstack/app-assume-policy.json:/etc/localstack/init/ready.d/app-assume-policy.json'
|
38
|
+
- './local/localstack/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh'
|
19
39
|
init:
|
20
40
|
image: curlimages/curl
|
21
41
|
depends_on:
|
22
42
|
- vault
|
23
43
|
- db
|
44
|
+
- aws
|
24
45
|
volumes:
|
25
46
|
- './local/vault/setup.sh:/etc/vault/setup.sh'
|
26
47
|
environment:
|
@@ -23,8 +23,8 @@ module SettingsReader
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def fetch(address, &block)
|
26
|
-
if !address.no_cache? && (
|
27
|
-
|
26
|
+
if !address.no_cache? && (existing_entry = retrieve(address))
|
27
|
+
existing_entry
|
28
28
|
else
|
29
29
|
new_entry = block.call(address)
|
30
30
|
save(new_entry) if new_entry
|
@@ -6,6 +6,10 @@ module SettingsReader
|
|
6
6
|
# Default: Logger.new(STDOUT, level: Logger::ERROR)
|
7
7
|
attr_accessor :logger
|
8
8
|
|
9
|
+
# What errors should be retried when connecting to vault
|
10
|
+
# Default: `Vault::HTTPConnectionError` and `OpenSSL::SSL::SSLError`
|
11
|
+
attr_accessor :retriable_errors
|
12
|
+
|
9
13
|
# How many times to retry retrieval of the secret
|
10
14
|
# Default: 2
|
11
15
|
attr_accessor :retrieval_retries
|
@@ -40,6 +44,7 @@ module SettingsReader
|
|
40
44
|
|
41
45
|
def initialize
|
42
46
|
@logger = Logger.new($stdout, level: Logger::ERROR)
|
47
|
+
@retriable_errors = [OpenSSL::SSL::SSLError, Vault::HTTPConnectionError]
|
43
48
|
@retrieval_retries = 2
|
44
49
|
@lease_refresh_interval = 60
|
45
50
|
@lease_renew_delay = 300
|
@@ -65,6 +70,7 @@ module SettingsReader
|
|
65
70
|
@vault_engines ||= [
|
66
71
|
SettingsReader::VaultResolver::Engines::KV2.new(self),
|
67
72
|
SettingsReader::VaultResolver::Engines::Database.new(self),
|
73
|
+
SettingsReader::VaultResolver::Engines::Aws.new(self),
|
68
74
|
SettingsReader::VaultResolver::Engines::Auth.new(self)
|
69
75
|
]
|
70
76
|
end
|
@@ -24,7 +24,7 @@ module SettingsReader
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def renew(entry)
|
27
|
-
return unless entry.
|
27
|
+
return unless entry.renewable?
|
28
28
|
|
29
29
|
new_secret = renew_and_retry_auth(entry)
|
30
30
|
entry.update_renewed(new_secret)
|
@@ -43,7 +43,7 @@ module SettingsReader
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def get_and_retry_connection(address)
|
46
|
-
Vault.with_retries(
|
46
|
+
Vault.with_retries(*config.retriable_errors, attempts: config.retrieval_retries) do
|
47
47
|
get_secret(address)
|
48
48
|
end
|
49
49
|
end
|
@@ -58,7 +58,7 @@ module SettingsReader
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def renew_and_retry_connection(entry)
|
61
|
-
Vault.with_retries(
|
61
|
+
Vault.with_retries(*config.retriable_errors, attempts: config.lease_renew_retries) do
|
62
62
|
renew_lease(entry)
|
63
63
|
end
|
64
64
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module SettingsReader
|
2
|
+
module VaultResolver
|
3
|
+
module Engines
|
4
|
+
# Adapter to retrieve / renew secret from database engine
|
5
|
+
class Aws < Abstract
|
6
|
+
MOUNT = 'aws'.freeze
|
7
|
+
|
8
|
+
def retrieves?(address)
|
9
|
+
address.mount == MOUNT
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def get_secret(address)
|
15
|
+
debug { "Fetching new aws secret at: #{address}" }
|
16
|
+
Vault.logical.read(address.full_path)
|
17
|
+
rescue Vault::HTTPClientError => e
|
18
|
+
return nil if e.message.match?('Role ".*" not found')
|
19
|
+
|
20
|
+
raise e
|
21
|
+
end
|
22
|
+
|
23
|
+
def renew_lease(entry)
|
24
|
+
Vault.sys.renew(entry.lease_id)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -13,10 +13,14 @@ module SettingsReader
|
|
13
13
|
@lease_started = Time.now
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def renewable?
|
17
17
|
@secret.renewable?
|
18
18
|
end
|
19
19
|
|
20
|
+
def leased?
|
21
|
+
renewable? || @secret.lease_duration&.positive?
|
22
|
+
end
|
23
|
+
|
20
24
|
def expired?
|
21
25
|
return false unless leased?
|
22
26
|
|
@@ -19,7 +19,7 @@ module SettingsReader
|
|
19
19
|
def refresh
|
20
20
|
info { 'Performing Vault leases refresh' }
|
21
21
|
promises = cache.active_entries.map do |entry|
|
22
|
-
debug { "Checking lease for #{entry}.
|
22
|
+
debug { "Checking lease for #{entry}. Renewable?: #{entry.renewable?}. Expires in: #{entry.expires_in}s" }
|
23
23
|
refresh_entry(entry)
|
24
24
|
end.compact
|
25
25
|
promises.each(&:wait)
|
@@ -11,6 +11,7 @@ require_relative 'vault_resolver/engines/abstract'
|
|
11
11
|
require_relative 'vault_resolver/engines/auth'
|
12
12
|
require_relative 'vault_resolver/engines/kv2'
|
13
13
|
require_relative 'vault_resolver/engines/database'
|
14
|
+
require_relative 'vault_resolver/engines/aws'
|
14
15
|
require_relative 'vault_resolver/cache'
|
15
16
|
require_relative 'vault_resolver/refresher'
|
16
17
|
require_relative 'vault_resolver/refresher_observer'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: settings_reader-vault_resolver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Volodymyr Mykhailyk
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -62,7 +62,6 @@ executables: []
|
|
62
62
|
extensions: []
|
63
63
|
extra_rdoc_files: []
|
64
64
|
files:
|
65
|
-
- ".circleci/config.yml"
|
66
65
|
- ".github/workflows/linters.yml"
|
67
66
|
- ".github/workflows/main.yml"
|
68
67
|
- ".gitignore"
|
@@ -86,6 +85,7 @@ files:
|
|
86
85
|
- lib/settings_reader/vault_resolver/configuration.rb
|
87
86
|
- lib/settings_reader/vault_resolver/engines/abstract.rb
|
88
87
|
- lib/settings_reader/vault_resolver/engines/auth.rb
|
88
|
+
- lib/settings_reader/vault_resolver/engines/aws.rb
|
89
89
|
- lib/settings_reader/vault_resolver/engines/database.rb
|
90
90
|
- lib/settings_reader/vault_resolver/engines/kv2.rb
|
91
91
|
- lib/settings_reader/vault_resolver/entry.rb
|
@@ -119,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
119
|
- !ruby/object:Gem::Version
|
120
120
|
version: '0'
|
121
121
|
requirements: []
|
122
|
-
rubygems_version: 3.
|
122
|
+
rubygems_version: 3.1.6
|
123
123
|
signing_key:
|
124
124
|
specification_version: 4
|
125
125
|
summary: Settings Reader plugin to resolve values using in Hashicorp Vault
|
data/.circleci/config.yml
DELETED
@@ -1,114 +0,0 @@
|
|
1
|
-
|
2
|
-
version: 2.1
|
3
|
-
|
4
|
-
orbs:
|
5
|
-
ci: matic/orb-common@0.2
|
6
|
-
ruby: circleci/ruby@1.8.0
|
7
|
-
|
8
|
-
jobs:
|
9
|
-
|
10
|
-
rspec-test:
|
11
|
-
resource_class: small
|
12
|
-
parameters:
|
13
|
-
ruby-version:
|
14
|
-
type: string
|
15
|
-
docker:
|
16
|
-
- image: cimg/ruby:<< parameters.ruby-version >>
|
17
|
-
environment:
|
18
|
-
COVERAGE: true
|
19
|
-
CODECOV_TOKEN: a0c859b6-dfb7-4d9f-9933-2dd945cdd960
|
20
|
-
VAULT_ADDR: 'http://127.0.0.1:8200'
|
21
|
-
VAULT_TOKEN: 'vault_root_token'
|
22
|
-
- image: vault
|
23
|
-
environment:
|
24
|
-
VAULT_DEV_ROOT_TOKEN_ID: vault_root_token
|
25
|
-
SKIP_SETCAP: true
|
26
|
-
- image: postgres:14.1-alpine
|
27
|
-
environment:
|
28
|
-
POSTGRES_DB: 'app_db'
|
29
|
-
POSTGRES_USER: 'vault_root'
|
30
|
-
POSTGRES_PASSWORD: 'root_password'
|
31
|
-
steps:
|
32
|
-
- checkout
|
33
|
-
- ruby/install-deps
|
34
|
-
- run:
|
35
|
-
name: Set up vault
|
36
|
-
command: sh local/vault/setup.sh
|
37
|
-
- run:
|
38
|
-
name: Run RSpec Tests
|
39
|
-
command: bundle exec rspec
|
40
|
-
- store_test_results:
|
41
|
-
path: reports/rspec
|
42
|
-
- store_artifacts:
|
43
|
-
path: reports/rspec
|
44
|
-
- ci/slack-stage-message
|
45
|
-
|
46
|
-
rubocop:
|
47
|
-
resource_class: small
|
48
|
-
docker:
|
49
|
-
- image: cimg/ruby:2.5
|
50
|
-
steps:
|
51
|
-
- checkout
|
52
|
-
- ruby/install-deps
|
53
|
-
- run:
|
54
|
-
name: Run rubocop
|
55
|
-
command: bundle exec rubocop --parallel
|
56
|
-
|
57
|
-
release:
|
58
|
-
parameters:
|
59
|
-
tag:
|
60
|
-
type: string
|
61
|
-
default: "default-tag"
|
62
|
-
docker:
|
63
|
-
- image: cimg/ruby:2.7.5
|
64
|
-
environment:
|
65
|
-
RELEASE_TAG: << parameters.tag >>
|
66
|
-
steps:
|
67
|
-
- checkout
|
68
|
-
- ruby/install-deps
|
69
|
-
- run:
|
70
|
-
name: Set up credentials
|
71
|
-
command: |
|
72
|
-
mkdir -p $HOME/.gem
|
73
|
-
touch $HOME/.gem/credentials
|
74
|
-
chmod 0600 $HOME/.gem/credentials
|
75
|
-
printf -- "---\n:rubygems_api_key: $RUBYGEMS_API_KEY\n" > $HOME/.gem/credentials
|
76
|
-
- run:
|
77
|
-
name: Set version
|
78
|
-
command: sed -i "s/[[:digit:]].[[:digit:]].[[:digit:]]/${RELEASE_TAG}/g" $(find . -name "version.rb")
|
79
|
-
- run:
|
80
|
-
name: Build gem
|
81
|
-
command: gem build *.gemspec
|
82
|
-
- run:
|
83
|
-
name: Push gem
|
84
|
-
command: gem push *.gem
|
85
|
-
|
86
|
-
workflows:
|
87
|
-
|
88
|
-
settings_reader-vault_resolver.build-pull-request:
|
89
|
-
when:
|
90
|
-
not:
|
91
|
-
equal: [ main, << pipeline.git.branch >> ]
|
92
|
-
jobs:
|
93
|
-
|
94
|
-
- rspec-test:
|
95
|
-
context: global
|
96
|
-
matrix:
|
97
|
-
parameters:
|
98
|
-
ruby-version: [ '2.5', '2.6', '2.7', '3.0' ]
|
99
|
-
|
100
|
-
- rubocop:
|
101
|
-
name: Rubocop
|
102
|
-
context: global
|
103
|
-
|
104
|
-
settings_reader-vault_resolver.release:
|
105
|
-
jobs:
|
106
|
-
|
107
|
-
- release:
|
108
|
-
tag: << pipeline.git.tag >>
|
109
|
-
context: gem-publishing
|
110
|
-
filters:
|
111
|
-
branches:
|
112
|
-
ignore: /.*/
|
113
|
-
tags:
|
114
|
-
only: /\d\.\d\.\d/ # It should be [digin dot digit dot digit] format
|