gitops-image-replacer 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,9 @@
1
+ """
2
+ gitops-image-replacer
3
+
4
+ Automated container image updates for GitOps repositories via GitHub API.
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+ __author__ = "Simon Lauger"
9
+ __email__ = "simon@lauger.de"
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env python3
2
+ # gitops-image-replacer
3
+ #
4
+ # Accepts a container image format as argument and scans and updates
5
+ # the tag and/or the digest of an image url in one or more GitHub repositories.
6
+ #
7
+ # Author: Simon Lauger <simon@lauger.de>
8
+ #
9
+ # Notes:
10
+ # - Defaults to JSON config (gitops-image-replacer.json). YAML is supported as fallback.
11
+ # - Uses safe regex replacement with re.escape(image_name).
12
+ # - Uses requests.Session with retry & timeout.
13
+ # - Avoids printing full file contents unless --verbose is set.
14
+ #
15
+ from pathlib import Path
16
+ import requests
17
+ import json
18
+ import os
19
+ import sys
20
+ import re
21
+ import base64
22
+ import argparse
23
+ import urllib.parse
24
+ import yaml
25
+ from yaml.loader import SafeLoader
26
+ from requests.adapters import HTTPAdapter
27
+ from urllib3.util.retry import Retry
28
+
29
+ DEFAULT_CONFIG = "gitops-image-replacer.json"
30
+ TAG_DIGEST_PATTERN = r'(:(?P<tag>[\w.\-_]{1,127})|)?(@(?P<digest>sha256:[a-f0-9]{64}))?'
31
+
32
+ def make_session():
33
+ sess = requests.Session()
34
+ retries = Retry(
35
+ total=5,
36
+ read=5,
37
+ connect=5,
38
+ backoff_factor=0.5,
39
+ status_forcelist=(429, 500, 502, 503, 504),
40
+ allowed_methods=frozenset(["GET", "PUT", "HEAD"]),
41
+ raise_on_status=False
42
+ )
43
+ adapter = HTTPAdapter(max_retries=retries)
44
+ sess.mount("https://", adapter)
45
+ sess.mount("http://", adapter)
46
+ return sess
47
+
48
+ def main():
49
+ parser = argparse.ArgumentParser(description='command line arguments.')
50
+ parser.add_argument(
51
+ '--config',
52
+ metavar='<file>',
53
+ type=str,
54
+ help=f'configuration file (defaults to "{DEFAULT_CONFIG}")',
55
+ default=DEFAULT_CONFIG,
56
+ required=False,
57
+ )
58
+ parser.add_argument(
59
+ '--apply',
60
+ action='store_true',
61
+ help='if set the changes will be applied to the repository, otherwise the script runs in dry-run mode',
62
+ required=False,
63
+ )
64
+ parser.add_argument(
65
+ '--ci',
66
+ action='store_true',
67
+ help='enable the CI mode, which validates the environment variable GIT_REF against patterns in the config file',
68
+ required=False,
69
+ )
70
+ parser.add_argument(
71
+ 'image',
72
+ metavar='<string>',
73
+ help='docker image (e.g. docker.io/foo/bar:2.0.0)',
74
+ type=str,
75
+ default=None,
76
+ )
77
+ parser.add_argument('--name',
78
+ metavar='<string>',
79
+ help='author name which is used during the commit of the changes (env: GIT_COMMIT_NAME)',
80
+ type=str,
81
+ default=os.getenv('GIT_COMMIT_NAME', 'Replacer Bot'),
82
+ )
83
+ parser.add_argument('--email',
84
+ metavar='<string>',
85
+ help='email which is used during the commit of the changes (env: GIT_COMMIT_EMAIL)',
86
+ type=str,
87
+ default=os.getenv('GIT_COMMIT_EMAIL', 'replacer-bot@localhost.localdomain'),
88
+ )
89
+ parser.add_argument('--message',
90
+ metavar='<string>',
91
+ help='commit message template (default to "fix: update image to {}")',
92
+ type=str,
93
+ default='fix: update image to {}',
94
+ )
95
+ parser.add_argument('--api',
96
+ metavar='<string>',
97
+ help='URL to the GitHub API (default: "https://api.github.com"; env: GITHUB_API_URL)',
98
+ type=str,
99
+ default=os.getenv('GITHUB_API_URL', 'https://api.github.com'),
100
+ )
101
+ parser.add_argument('--verbose',
102
+ action='store_true',
103
+ help='enable verbose logging (prints file contents and desired state)',
104
+ required=False,
105
+ )
106
+
107
+ args = parser.parse_args()
108
+
109
+ # get variables from environment
110
+ git_ref = os.getenv('GIT_REF', None)
111
+ github_token = os.getenv('GITHUB_TOKEN', None)
112
+
113
+ # validate variables
114
+ if not github_token:
115
+ print("error: GITHUB_TOKEN is not set")
116
+ sys.exit(1)
117
+
118
+ if args.ci and not git_ref:
119
+ print("error: GIT_REF is not set (required in --ci mode)")
120
+ sys.exit(1)
121
+
122
+ # load config file
123
+ if not os.path.exists(args.config):
124
+ print(f"error: config file {args.config} does not exist")
125
+ sys.exit(1)
126
+
127
+ with open(args.config, 'r') as f:
128
+ if args.config.endswith('.json'):
129
+ config = json.load(f)
130
+ else:
131
+ config = yaml.load(f, Loader=SafeLoader)
132
+
133
+ if 'gitops-image-replacer' not in config:
134
+ print("info: no gitops-image-replacer entry found in config, exiting")
135
+ sys.exit(0)
136
+
137
+ # validate image and capture components
138
+ image_re = r'(?P<repository>[\w.\-_]+((?::\d+|)(?=/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+))|)(?:/|)(?P<image>[a-zA-Z0-9.\-_]+(?:/[a-zA-Z0-9.\-_]+|))' + TAG_DIGEST_PATTERN
139
+ result = re.search(image_re, args.image)
140
+ if not result:
141
+ print(f"error: it seems that '{args.image}' is not a valid image url")
142
+ sys.exit(1)
143
+
144
+ repository_part = result.group('repository') or ''
145
+ image_name = (repository_part + '/' if repository_part else '') + result.group('image')
146
+
147
+ print("info: run replacer for image " + image_name)
148
+
149
+ # default to exit code 0
150
+ exit_code = 0
151
+
152
+ if not args.apply:
153
+ print("info: running in dry-run, no changes will be applied")
154
+
155
+ session = make_session()
156
+ headers = {
157
+ 'Authorization': f'token {github_token}',
158
+ 'Accept': 'application/vnd.github.v3+json',
159
+ }
160
+ timeout = 30
161
+
162
+ # precheck block (cache responses for later reuse)
163
+ cache = {}
164
+ for repo in config['gitops-image-replacer']:
165
+ repo_path = repo['repository']
166
+ branch = repo['branch']
167
+ file_path = repo['file']
168
+ cache_key = f"{repo_path}:{branch}:{file_path}"
169
+
170
+ print(f"info: validate if {file_path} from repository {repo_path} in branch {branch} exists")
171
+
172
+ # Use GET for reliability; do not decode content here
173
+ url = f"{args.api}/repos/{repo_path}/contents/{file_path}?ref={urllib.parse.quote(branch)}"
174
+ if args.verbose:
175
+ print(url)
176
+ precheck = session.get(url, headers=headers, timeout=timeout)
177
+ if precheck.status_code == 401:
178
+ print("error: 401 unauthorized - maybe your token does not have access to the defined repository")
179
+ exit_code = 1
180
+ elif precheck.status_code == 404:
181
+ print("error: 404 not found - make sure that the file exists in the defined target")
182
+ exit_code = 1
183
+ elif precheck.status_code != 200:
184
+ print(f"error: unknown error with HTTP code {precheck.status_code}")
185
+ exit_code = 1
186
+ else:
187
+ # Cache successful response for later reuse
188
+ cache[cache_key] = precheck.json()
189
+
190
+ if exit_code != 0:
191
+ sys.exit(exit_code)
192
+
193
+ # replace block
194
+ for repo in config['gitops-image-replacer']:
195
+ repo_path = repo['repository']
196
+ branch = repo['branch']
197
+ file_path = repo['file']
198
+
199
+ if args.ci:
200
+ if 'when' in repo:
201
+ if not re.match(repo['when'], git_ref or ""):
202
+ print(f"info: git-ref {git_ref} does not match when pattern ('{repo['when']}')")
203
+ continue
204
+ else:
205
+ print(f"info: git-ref {git_ref} matches when pattern ('{repo['when']}')")
206
+ if 'except' in repo:
207
+ if re.match(repo['except'], git_ref or ""):
208
+ print(f"info: git-ref {git_ref} matches except pattern ('{repo['except']}')")
209
+ continue
210
+ else:
211
+ print(f"info: git-ref {git_ref} does not match except pattern ('{repo['except']}')")
212
+
213
+ # get file (reuse cached data from precheck if available)
214
+ cache_key = f"{repo_path}:{branch}:{file_path}"
215
+ if cache_key in cache:
216
+ print(f"info: using cached data for {file_path} from repository {repo_path}")
217
+ fetch_json = cache[cache_key]
218
+ else:
219
+ print(f"info: fetch {file_path} from repository {repo_path} in branch {branch}")
220
+ fetch_url = f"{args.api}/repos/{repo_path}/contents/{file_path}?ref={urllib.parse.quote(branch)}"
221
+ fetch = session.get(fetch_url, headers=headers, timeout=timeout)
222
+ if fetch.status_code != 200:
223
+ try:
224
+ fetch_json = fetch.json()
225
+ msg = fetch_json.get('message', 'unknown error')
226
+ except Exception:
227
+ msg = 'unknown error'
228
+ print(f"error: {msg}")
229
+ exit_code = 1
230
+ continue
231
+ fetch_json = fetch.json()
232
+
233
+ content_original = base64.b64decode(fetch_json['content']).decode('utf-8')
234
+
235
+ if args.verbose:
236
+ print(f"info: original content of {file_path}:")
237
+ print(f"#### BEGIN OF SOURCE FILE {file_path} ####")
238
+ print(content_original)
239
+ print(f"#### END OF SOURCE FILE {file_path} ####")
240
+
241
+ # replace image in target file
242
+ pattern = re.escape(image_name) + TAG_DIGEST_PATTERN
243
+ content = re.sub(pattern, args.image, content_original)
244
+
245
+ # check for changes
246
+ if content == content_original:
247
+ print(f"info: no outstanding changes are found in file {file_path}")
248
+ continue
249
+
250
+ if args.verbose:
251
+ print(f"info: desired content of {file_path}:")
252
+ print(f"#### BEGIN OF DESIRED FILE {file_path} ####")
253
+ print(content)
254
+ print(f"#### END OF DESIRED FILE {file_path} ####")
255
+
256
+ if not args.apply:
257
+ continue
258
+
259
+ # update file in repository
260
+ print(f"info: update {file_path} from repository {repo_path} in branch {branch}")
261
+ put_url = f"{args.api}/repos/{repo_path}/contents/{file_path}"
262
+ update = session.put(
263
+ put_url,
264
+ headers={**headers, 'Content-Type': 'application/json'},
265
+ data=json.dumps({
266
+ 'committer': {
267
+ 'name': args.name,
268
+ 'email': args.email,
269
+ },
270
+ 'message': args.message.format(args.image),
271
+ 'branch': branch,
272
+ 'content': base64.b64encode(content.encode('utf-8')).decode(),
273
+ 'sha': fetch_json['sha']
274
+ }),
275
+ timeout=timeout
276
+ )
277
+
278
+ try:
279
+ update_json = update.json()
280
+ print(json.dumps(update_json, indent=4))
281
+ except Exception:
282
+ print("warn: could not decode update response as JSON")
283
+
284
+ if update.status_code not in (200, 201):
285
+ exit_code = 1
286
+
287
+ sys.exit(exit_code)
288
+
289
+ if __name__ == "__main__":
290
+ main()
@@ -0,0 +1,370 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitops-image-replacer
3
+ Version: 1.0.0
4
+ Summary: Automated container image updates for GitOps repositories via GitHub API
5
+ Author-email: Simon Lauger <simon@lauger.de>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/slauger/gitops-image-replacer
8
+ Project-URL: Documentation, https://github.com/slauger/gitops-image-replacer#readme
9
+ Project-URL: Repository, https://github.com/slauger/gitops-image-replacer
10
+ Project-URL: Issues, https://github.com/slauger/gitops-image-replacer/issues
11
+ Project-URL: Changelog, https://github.com/slauger/gitops-image-replacer/blob/main/CHANGELOG.md
12
+ Keywords: gitops,docker,kubernetes,container,image,automation,github,ci-cd,deployment
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: License :: OSI Approved :: MIT License
23
+ Classifier: Operating System :: OS Independent
24
+ Classifier: Topic :: Software Development :: Build Tools
25
+ Classifier: Topic :: System :: Systems Administration
26
+ Classifier: Topic :: Utilities
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: requests>=2.25.0
31
+ Requires-Dist: PyYAML>=5.4.0
32
+ Dynamic: license-file
33
+
34
+ # gitops-image-replacer
35
+
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
38
+ [![PyPI version](https://img.shields.io/pypi/v/gitops-image-replacer.svg)](https://pypi.org/project/gitops-image-replacer/)
39
+ [![PyPI downloads](https://img.shields.io/pypi/dm/gitops-image-replacer.svg)](https://pypi.org/project/gitops-image-replacer/)
40
+
41
+ A lightweight CLI tool that automates container image updates in GitOps repositories. Replace image tags and/or digests across multiple GitHub repositories with a single command, enabling automated deployment workflows.
42
+
43
+ ## Features
44
+
45
+ - **Flexible Modes**: Dry-run for validation, apply mode for commits
46
+ - **CI/CD Integration**: Built-in CI mode with `GIT_REF` pattern matching
47
+ - **Complete Image Support**: Handles tags, digests, and combined references (e.g., `registry.io/ns/app:1.2.3@sha256:...`)
48
+ - **Multiple Repositories**: Update images across any number of repos and files
49
+ - **Configuration Formats**: JSON (default) and YAML support
50
+ - **Performance Optimized**: Response caching eliminates duplicate API calls
51
+ - **Safe Operations**: Regex escaping prevents unintended replacements
52
+ - **Robust HTTP**: Automatic retries, timeouts, and error handling
53
+ - **Clean Logging**: Minimal output by default, verbose mode for debugging
54
+
55
+ ## Requirements
56
+
57
+ - Python 3.8+
58
+ - A GitHub/GitHub Enterprise token with content read/write access
59
+
60
+ ## Installation
61
+
62
+ ### Via pip (recommended)
63
+
64
+ ```bash
65
+ # Install from PyPI
66
+ pip install gitops-image-replacer
67
+
68
+ # Verify installation
69
+ gitops-image-replacer --help
70
+ ```
71
+
72
+ ### From source
73
+
74
+ ```bash
75
+ # Clone repository
76
+ git clone https://github.com/slauger/gitops-image-replacer.git
77
+ cd gitops-image-replacer
78
+
79
+ # Install in development mode
80
+ pip install -e .
81
+
82
+ # Or run directly
83
+ python -m gitops_image_replacer --help
84
+ ```
85
+
86
+ ## Quick Start
87
+
88
+ 1. Create a configuration file (default: `gitops-image-replacer.json`).
89
+ 2. Run a dry-run:
90
+ ```bash
91
+ gitops-image-replacer docker.io/example/app:2.0.0
92
+
93
+ gitops-image-replacer docker.io/example/app:2.0.0@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
94
+
95
+ gitops-image-replacer docker.io/example/app@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
96
+
97
+ gitops-image-replacer --config gitops-image-replacer.json docker.io/example/app:2.0.0
98
+ ```
99
+ 3. Apply changes (commit to target repos):
100
+ ```bash
101
+ gitops-image-replacer --apply docker.io/example/app:2.0.0
102
+
103
+ gitops-image-replacer --config gitops-image-replacer.json --apply docker.io/example/app:2.0.0
104
+ ```
105
+
106
+ ## CLI
107
+
108
+ ```text
109
+ usage: gitops-image-replacer [-h] [--config <file>] [--apply] [--ci]
110
+ [--name <string>] [--email <string>]
111
+ [--message <string>] [--api <string>]
112
+ [--verbose]
113
+ <string>
114
+ ```
115
+
116
+ - `--config` Path to the configuration file (default: `gitops-image-replacer.json`). JSON recommended.
117
+ - `--apply` Apply changes (commit). Without this flag the tool runs in dry-run.
118
+ - `--ci` CI mode: validates `GIT_REF` against `when`/`except` regex patterns from config.
119
+ - `--name` Commit author name (default: env `GIT_COMMIT_NAME` or `Replacer Bot`).
120
+ - `--email` Commit author email (default: env `GIT_COMMIT_EMAIL` or `replacer-bot@localhost.localdomain`).
121
+ - `--message` Commit message template (default: `fix: update image to {}`).
122
+ - `--api` GitHub API URL (default: env `GITHUB_API_URL` or `https://github.com/api/v3`).
123
+ - `--verbose` Print file contents and desired state (use with care in CI logs).
124
+ - Positional: `image` (e.g., `docker.io/foo/bar:2.0.0`). Tag and digest are optional.
125
+
126
+ ### Environment
127
+
128
+ - `GITHUB_TOKEN` **(required)** – token with access to read/write repository contents.
129
+ - `GIT_REF` *(required when `--ci`)* – the current ref string, e.g., `refs/heads/main`.
130
+
131
+ Recommended token scopes:
132
+ - Public repos only: `public_repo`
133
+ - Private repos: `repo`
134
+ - GitHub Enterprise: equivalent content permissions
135
+
136
+ ## Configuration
137
+
138
+ Default format is **JSON**. YAML (`.yaml`/`.yml`) is supported as well.
139
+
140
+ ### JSON schema (per entry)
141
+
142
+ ```json
143
+ {
144
+ "gitops-image-replacer": [
145
+ {
146
+ "repository": "org/repo",
147
+ "branch": "main",
148
+ "file": "path/to/values.yaml",
149
+ "when": "^refs/heads/(main|release/.*)$",
150
+ "except": "^refs/heads/feature/"
151
+ }
152
+ ]
153
+ }
154
+ ```
155
+
156
+ **Fields**
157
+
158
+ - `repository` (string): `ORG/REPO` path on GitHub/GHE
159
+ - `branch` (string): target branch
160
+ - `file` (string): target file path in the repository
161
+ - `when` (string, optional): regex that must match `GIT_REF` when `--ci` is enabled
162
+ - `except` (string, optional): regex that must **not** match `GIT_REF` when `--ci` is enabled
163
+
164
+ > The tool uses `re.match` (anchored at the string start). Use `^...$` in your patterns if you require a full match.
165
+
166
+ ### Examples
167
+
168
+ **JSON (default)**
169
+
170
+ ```json
171
+ {
172
+ "gitops-image-replacer": [
173
+ {
174
+ "repository": "acme/online-shop",
175
+ "branch": "main",
176
+ "file": "deploy/values.yaml",
177
+ "when": "^refs/heads/(main|release/.*)$"
178
+ },
179
+ {
180
+ "repository": "acme/payments",
181
+ "branch": "develop",
182
+ "file": "charts/payments/values.yaml",
183
+ "except": "^refs/heads/legacy/"
184
+ }
185
+ ]
186
+ }
187
+ ```
188
+
189
+ **YAML (alternative)**
190
+
191
+ ```yaml
192
+ gitops-image-replacer:
193
+ - repository: acme/online-shop
194
+ branch: main
195
+ file: deploy/values.yaml
196
+ when: '^refs/heads/(main|release/.*)$'
197
+ - repository: acme/payments
198
+ branch: develop
199
+ file: charts/payments/values.yaml
200
+ except: '^refs/heads/legacy/'
201
+ ```
202
+
203
+ ## Supported Image Formats
204
+
205
+ The tool validates and matches container images using a comprehensive regex pattern. All components are optional except the image name.
206
+
207
+ ### Regex Pattern
208
+
209
+ ```regex
210
+ (?P<repository>[\w.\-_]+((?::\d+|)(?=/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+))|)(?:/|)(?P<image>[a-zA-Z0-9.\-_]+(?:/[a-zA-Z0-9.\-_]+|))(:(?P<tag>[\w.\-_]{1,127})|)?(@(?P<digest>sha256:[a-f0-9]{64}))?
211
+ ```
212
+
213
+ ### Pattern Components
214
+
215
+ - **Repository/Registry** (optional): `docker.io`, `gcr.io`, `registry.example.com:5000`
216
+ - Supports hostnames with optional ports
217
+ - Alphanumeric, dots, dashes, underscores
218
+ - **Image Name** (required): `library/nginx`, `myorg/myapp`, `MyApp`
219
+ - Case-sensitive (supports both uppercase and lowercase)
220
+ - Can include organization/namespace
221
+ - **Tag** (optional): `:latest`, `:v1.2.3`, `:20241125-abc123`
222
+ - Max 127 characters
223
+ - Alphanumeric, dots, dashes, underscores
224
+ - **Digest** (optional): `@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1`
225
+ - SHA256 hash (64 hex characters, lowercase)
226
+
227
+ ### Valid Examples
228
+
229
+ ```bash
230
+ # Simple image name
231
+ nginx
232
+
233
+ # With registry and tag
234
+ docker.io/library/nginx:1.25
235
+
236
+ # With digest only
237
+ gcr.io/myproject/myapp@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
238
+
239
+ # Tag and digest (immutable reference)
240
+ registry.example.com:5000/org/app:v2.0.0@sha256:abc...
241
+
242
+ # Case-sensitive names
243
+ ghcr.io/MyOrg/MyApp:latest
244
+ ```
245
+
246
+ ## How it works
247
+
248
+ 1. **Validation**: Checks CLI arguments, environment variables, and configuration file
249
+ 2. **Precheck Phase**: Validates access to all target repositories/files (caches responses)
250
+ 3. **Replace Phase**: Downloads files (reuses cached data), performs safe regex replacement
251
+ 4. **Commit Phase**: If `--apply` is set and changes detected, commits via GitHub Contents API
252
+ 5. **Exit Codes**: Returns `0` on success, non-zero on failures
253
+
254
+ ### Performance Optimization
255
+
256
+ The tool caches file contents from the precheck phase, eliminating duplicate API calls during the replace phase. This reduces GitHub API usage by approximately 50% in typical scenarios.
257
+
258
+ ## Exit Codes
259
+
260
+ - `0` success (no changes or committed changes)
261
+ - `1` validation or API error
262
+
263
+ ## Best Practices
264
+
265
+ ### Testing and Validation
266
+ - **Always start with dry-run**: Test your configuration without `--apply` first
267
+ - **Use verbose mode carefully**: `--verbose` prints file contents - avoid in CI logs with sensitive data
268
+ - **Validate regex patterns**: Use `^...$` anchors in `when`/`except` for full string matches
269
+
270
+ ### Configuration Management
271
+ - **Keep config versioned**: Store `gitops-image-replacer.json` in your repository
272
+ - **Use meaningful names**: Standard naming makes the file's purpose obvious
273
+ - **Document patterns**: Comment your regex patterns (especially in CI mode)
274
+
275
+ ### Security
276
+ - **Limit token scope**: Use minimal permissions (contents: write)
277
+ - **Rotate tokens**: Regularly update GitHub tokens
278
+ - **Review changes**: Use dry-run before production deployments
279
+
280
+ ### Performance
281
+ - **Minimize targets**: Only configure files that need updates
282
+ - **Use CI patterns wisely**: `when`/`except` patterns reduce unnecessary runs
283
+ - **Leverage caching**: The tool automatically caches API responses
284
+
285
+ ## Troubleshooting
286
+
287
+ ### Common Issues
288
+
289
+ **401 Unauthorized**
290
+ - Verify `GITHUB_TOKEN` is set correctly
291
+ - Check token has `repo` or `public_repo` scope
292
+ - For GitHub Enterprise, confirm token has access to the organization
293
+
294
+ **404 Not Found**
295
+ - Verify `repository`, `branch`, and `file` paths in config
296
+ - Check branch name spelling (case-sensitive)
297
+ - Ensure file exists at the specified path
298
+
299
+ **No changes detected**
300
+ - Confirm target file contains the exact image name (case-sensitive)
301
+ - Tags and digests are optional in the search pattern
302
+ - Use `--verbose` to see file contents and verify the image reference format
303
+
304
+ **Rate limiting (429/403)**
305
+ - Built-in retries handle temporary rate limits
306
+ - Reduce execution frequency in CI/CD pipelines
307
+ - Consider using GitHub App tokens for higher limits
308
+
309
+ ### Debug Mode
310
+
311
+ Run with `--verbose` to see:
312
+ - Full API URLs being called
313
+ - Complete file contents before replacement
314
+ - Desired file contents after replacement
315
+
316
+ **Warning**: Verbose mode may expose sensitive data in logs.
317
+
318
+ ## Use Cases
319
+
320
+ ### Automated Deployment Pipeline
321
+
322
+ Update production manifests when a new image is built:
323
+
324
+ ```bash
325
+ # In your CI/CD pipeline after building image
326
+ gitops-image-replacer --apply docker.io/myorg/myapp:${CI_COMMIT_SHA}
327
+ ```
328
+
329
+ ### Multi-Environment Updates
330
+
331
+ Use CI mode to update different environments based on branch:
332
+
333
+ ```json
334
+ {
335
+ "gitops-image-replacer": [
336
+ {
337
+ "repository": "myorg/gitops-production",
338
+ "branch": "main",
339
+ "file": "apps/myapp/deployment.yaml",
340
+ "when": "^refs/heads/main$"
341
+ },
342
+ {
343
+ "repository": "myorg/gitops-staging",
344
+ "branch": "main",
345
+ "file": "apps/myapp/deployment.yaml",
346
+ "when": "^refs/heads/(main|release/.*)$"
347
+ }
348
+ ]
349
+ }
350
+ ```
351
+
352
+ ### Digest Pinning
353
+
354
+ Update images with immutable digest references:
355
+
356
+ ```bash
357
+ gitops-image-replacer --apply \
358
+ registry.example.com/myapp:v2.0.0@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
359
+ ```
360
+
361
+ ## Contributing
362
+
363
+ Contributions are welcome! Please ensure:
364
+ - Code follows existing style and patterns
365
+ - Changes are tested with both dry-run and apply modes
366
+ - Documentation is updated for new features
367
+
368
+ ## License
369
+
370
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,8 @@
1
+ gitops_image_replacer/__init__.py,sha256=duynX0gXa4GfPhn_B6s3MH_WIF7I2Viicc-OwRXjnpo,186
2
+ gitops_image_replacer/__main__.py,sha256=cspt6OShHH5Nm3MADjxQguqAF_RHyHQ8xa4_obl42ME,10412
3
+ gitops_image_replacer-1.0.0.dist-info/licenses/LICENSE,sha256=PWKv48zlqsEbvFv-3oXpPI8yO_I11cnIldtq87mZQrI,1069
4
+ gitops_image_replacer-1.0.0.dist-info/METADATA,sha256=SaBL3LIWKL6BpA_gxirYQyt9yAEtjl_EBRJRG5A-v2I,12622
5
+ gitops_image_replacer-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ gitops_image_replacer-1.0.0.dist-info/entry_points.txt,sha256=6sNtuT-93pi7oH41K8WCMv65cq57cCXL1FGcolT2-s4,78
7
+ gitops_image_replacer-1.0.0.dist-info/top_level.txt,sha256=CTvvin9b5I4Fz478IGQ-2tE2WaxwJXguQ3puNumfpn8,22
8
+ gitops_image_replacer-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gitops-image-replacer = gitops_image_replacer.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Simon Lauger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ gitops_image_replacer