vm-tool 1.0.42__tar.gz → 1.0.44__tar.gz
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.
- vm_tool-1.0.44/.agent/workflows/push.md +11 -0
- vm_tool-1.0.44/.agent/workflows/test_and_lint.md +18 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/PKG-INFO +1 -1
- {vm_tool-1.0.42 → vm_tool-1.0.44}/codePushToGithub.py +32 -6
- vm_tool-1.0.44/jingo.code-workspace +11 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/pyproject.toml +3 -6
- {vm_tool-1.0.42 → vm_tool-1.0.44}/setup.py +1 -1
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/cli.py +80 -1
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/generator.py +121 -37
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/secrets.py +75 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool.egg-info/PKG-INFO +1 -1
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool.egg-info/SOURCES.txt +3 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/.devcontainer/devcontainer.json +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/.github/dependabot.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/.github/workflows/ci.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/.github/workflows/deploy.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/.github/workflows/publish.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/.gitignore +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/.pre-commit-config.yaml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/CONTRIBUTING +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/CONTRIBUTING.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/LICENSE +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/MANIFEST.in +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/Makefile +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/README.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/MODULE_GUIDE.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/deployment-approaches.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/ec2-github-actions-guide.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/features.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/generator.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/index.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/pipeline-generator.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/reference/runner.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/reference/ssh.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/ssh-key-setup.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/docs/usage.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/README.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/__init__.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/cloud/README.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/cloud/__init__.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/cloud/ssh_identity_file.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/cloud/ssh_password.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/cloud/template_cloud_setup.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/deploy_full_setup.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/docker-compose.example.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/ec2-setup.sh +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/github-actions-ec2.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/github-actions-full-setup.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/local/.keep +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/local/README.md +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/local/__init__.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/local/template_local_setup.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/production-deploy.sh +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/rollback.sh +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/setup.sh +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/ssh_key_management.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/examples/version_check.sh +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/mkdocs.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/molecule/default/converge.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/molecule/default/molecule.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/molecule/default/verify.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/requirements-docs.txt +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/requirements.txt +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/runtime.txt +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/setup.cfg +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/conftest.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/integration/test_deployment.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/test_config.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/test_generator.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/test_health.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/test_history.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/test_logging.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/test_runner.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/test_ssh.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/tests/test_state.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/__init__.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/alerting.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/audit.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/backup.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/benchmarking.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/cloud.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/completion.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/compliance.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/config.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/drift.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/health.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/history.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/kubernetes.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/metrics.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/notifications.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/plugins.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/policy.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/rbac.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/recovery.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/reporting.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/runner.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/ssh.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/state.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/strategies/__init__.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/strategies/ab_testing.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/strategies/blue_green.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/strategies/canary.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/validation.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/cleanup.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/docker/create_docker_service.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/docker/docker_setup.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/docker/install_docker_and_compose.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/docker/login_to_docker_hub.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/github/git_configuration.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/inventory.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/k8s.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/main.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/monitoring.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/project_service.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/push_code.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/push_code_tasks.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/setup.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/vm_setup/setup_project_env.yml +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool/webhooks.py +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool.egg-info/dependency_links.txt +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool.egg-info/entry_points.txt +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool.egg-info/requires.txt +0 -0
- {vm_tool-1.0.42 → vm_tool-1.0.44}/vm_tool.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Push code using the custom automation script
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
This workflow uses the project's custom script to commit, bump version, and push changes. It ensures consistent versioning and environment management.
|
|
6
|
+
|
|
7
|
+
1. Run the custom push script
|
|
8
|
+
```bash
|
|
9
|
+
# Replace arguments as needed
|
|
10
|
+
python3 codePushToGithub.py --branch main --message "Update via Agent"
|
|
11
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run tests and linting for vm_tool
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
This workflow runs the project's tests and linting checks using the configured Makefile and Python tools.
|
|
6
|
+
|
|
7
|
+
1. Install dependencies (if needed) and run tests
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e ".[dev]"
|
|
11
|
+
pytest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
2. Run linting checks
|
|
15
|
+
```bash
|
|
16
|
+
flake8 vm_tool
|
|
17
|
+
black --check vm_tool
|
|
18
|
+
```
|
|
@@ -128,20 +128,38 @@ def get_current_version():
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
def main():
|
|
131
|
+
import argparse
|
|
132
|
+
|
|
133
|
+
parser = argparse.ArgumentParser(
|
|
134
|
+
description="Automate git commits and pushes with version bumping."
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument("--branch", "-b", help="Branch name to push to", default=None)
|
|
137
|
+
parser.add_argument("--message", "-m", help="Commit message", default=None)
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
|
|
131
140
|
# Setup virtual environment
|
|
132
141
|
print("🔧 Setting up environment...")
|
|
133
142
|
setup_environment()
|
|
134
143
|
print()
|
|
135
144
|
|
|
136
|
-
# Ask for the branch name
|
|
137
|
-
|
|
145
|
+
# Ask for the branch name if not provided
|
|
146
|
+
if args.branch:
|
|
147
|
+
branch_name = args.branch
|
|
148
|
+
else:
|
|
149
|
+
branch_name = input("Enter the branch name: ").strip()
|
|
138
150
|
|
|
139
|
-
if branch_name
|
|
151
|
+
if branch_name.lower() in ["main", "master"]:
|
|
140
152
|
# 1. Commit User Changes FIRST
|
|
141
153
|
print("Adding changes...")
|
|
142
154
|
run_command("git add .")
|
|
143
155
|
|
|
144
|
-
|
|
156
|
+
if args.message:
|
|
157
|
+
commit_message = args.message
|
|
158
|
+
else:
|
|
159
|
+
commit_message = input(
|
|
160
|
+
"Enter the commit message for your changes: "
|
|
161
|
+
).strip()
|
|
162
|
+
|
|
145
163
|
print("Committing changes...")
|
|
146
164
|
# Try to commit changes
|
|
147
165
|
result = run_command(f'git commit -m "{commit_message}"', check=False)
|
|
@@ -181,14 +199,22 @@ def main():
|
|
|
181
199
|
else:
|
|
182
200
|
# Create and switch to the new branch
|
|
183
201
|
print(f"Creating and switching to branch: {branch_name}")
|
|
184
|
-
|
|
202
|
+
# Check if branch exists
|
|
203
|
+
result = run_command(f"git rev-parse --verify {branch_name}", check=False)
|
|
204
|
+
if result.returncode == 0:
|
|
205
|
+
run_command(f"git checkout {branch_name}")
|
|
206
|
+
else:
|
|
207
|
+
run_command(f"git checkout -b {branch_name}")
|
|
185
208
|
|
|
186
209
|
# Add all changes
|
|
187
210
|
print("Adding changes...")
|
|
188
211
|
run_command("git add .")
|
|
189
212
|
|
|
190
213
|
# Ask for commit message
|
|
191
|
-
|
|
214
|
+
if args.message:
|
|
215
|
+
commit_message = args.message
|
|
216
|
+
else:
|
|
217
|
+
commit_message = input("Enter the commit message: ").strip()
|
|
192
218
|
|
|
193
219
|
# Commit with the entered message
|
|
194
220
|
print("Committing changes...")
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vm_tool"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.44"
|
|
8
8
|
description = "A Comprehensive Tool for Setting Up Virtual Machines."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -76,7 +76,7 @@ include = '\.pyi?$'
|
|
|
76
76
|
profile = "black"
|
|
77
77
|
multi_line_output = 3
|
|
78
78
|
[tool.bumpversion]
|
|
79
|
-
current_version = "1.0.
|
|
79
|
+
current_version = "1.0.44"
|
|
80
80
|
commit = true
|
|
81
81
|
tag = true
|
|
82
82
|
|
|
@@ -85,10 +85,7 @@ filename = "setup.py"
|
|
|
85
85
|
search = 'version="{current_version}"'
|
|
86
86
|
replace = 'version="{new_version}"'
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
filename = "vm_tool/cli.py"
|
|
90
|
-
search = 'version="{current_version}"'
|
|
91
|
-
replace = 'version="{new_version}"'
|
|
88
|
+
|
|
92
89
|
|
|
93
90
|
[[tool.bumpversion.files]]
|
|
94
91
|
filename = "pyproject.toml"
|
|
@@ -12,7 +12,7 @@ else:
|
|
|
12
12
|
|
|
13
13
|
setup(
|
|
14
14
|
name="vm_tool",
|
|
15
|
-
version="1.0.
|
|
15
|
+
version="1.0.44", # This will be updated by bump2version
|
|
16
16
|
packages=find_packages(),
|
|
17
17
|
description="A Comprehensive Tool for Setting Up Virtual Machines.",
|
|
18
18
|
long_description=long_description,
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import sys
|
|
3
|
+
import importlib.metadata
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
def main():
|
|
6
7
|
parser = argparse.ArgumentParser(
|
|
7
8
|
description="VM Tool: Setup, Provision, and Manage VMs"
|
|
8
9
|
)
|
|
9
|
-
|
|
10
|
+
try:
|
|
11
|
+
version = importlib.metadata.version("vm_tool")
|
|
12
|
+
except importlib.metadata.PackageNotFoundError:
|
|
13
|
+
version = "unknown"
|
|
14
|
+
parser.add_argument("--version", action="version", version=version)
|
|
10
15
|
parser.add_argument(
|
|
11
16
|
"--verbose", "-v", action="store_true", help="Enable verbose output"
|
|
12
17
|
)
|
|
@@ -294,6 +299,23 @@ def main():
|
|
|
294
299
|
help="CI/CD Platform",
|
|
295
300
|
)
|
|
296
301
|
|
|
302
|
+
# Secrets command
|
|
303
|
+
secrets_parser = subparsers.add_parser("secrets", help="Manage secrets")
|
|
304
|
+
secrets_subparsers = secrets_parser.add_subparsers(
|
|
305
|
+
dest="secrets_command", help="Secrets operations"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# secrets sync
|
|
309
|
+
sync_parser = secrets_subparsers.add_parser(
|
|
310
|
+
"sync", help="Sync local .env to GitHub Secrets"
|
|
311
|
+
)
|
|
312
|
+
sync_parser.add_argument(
|
|
313
|
+
"--env-file", type=str, default=".env", help="Path to .env file"
|
|
314
|
+
)
|
|
315
|
+
sync_parser.add_argument(
|
|
316
|
+
"--repo", type=str, help="Target GitHub repository (owner/repo)"
|
|
317
|
+
)
|
|
318
|
+
|
|
297
319
|
args = parser.parse_args()
|
|
298
320
|
|
|
299
321
|
# Configure logging based on flags
|
|
@@ -802,6 +824,63 @@ def main():
|
|
|
802
824
|
except Exception as e:
|
|
803
825
|
print(f"Error: {e}")
|
|
804
826
|
sys.exit(1)
|
|
827
|
+
elif args.command == "secrets":
|
|
828
|
+
if args.secrets_command == "sync":
|
|
829
|
+
from vm_tool.secrets import SecretsManager
|
|
830
|
+
import os
|
|
831
|
+
|
|
832
|
+
if not os.path.exists(args.env_file):
|
|
833
|
+
print(f"❌ Env file not found: {args.env_file}")
|
|
834
|
+
sys.exit(1)
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
manager = SecretsManager.from_github(repo=args.repo)
|
|
838
|
+
|
|
839
|
+
print(f"📖 Reading secrets from {args.env_file}...")
|
|
840
|
+
secrets_to_sync = {}
|
|
841
|
+
with open(args.env_file, "r") as f:
|
|
842
|
+
for line in f:
|
|
843
|
+
line = line.strip()
|
|
844
|
+
if not line or line.startswith("#"):
|
|
845
|
+
continue
|
|
846
|
+
# Basic env parsing
|
|
847
|
+
if "=" in line:
|
|
848
|
+
key, value = line.split("=", 1)
|
|
849
|
+
key = key.strip()
|
|
850
|
+
value = value.strip().strip("'").strip('"')
|
|
851
|
+
secrets_to_sync[key] = value
|
|
852
|
+
|
|
853
|
+
if not secrets_to_sync:
|
|
854
|
+
print("⚠️ No secrets found to sync.")
|
|
855
|
+
sys.exit(0)
|
|
856
|
+
|
|
857
|
+
print(f"🚀 Syncing {len(secrets_to_sync)} secrets to GitHub...")
|
|
858
|
+
# Confirm with user
|
|
859
|
+
confirm = (
|
|
860
|
+
input(
|
|
861
|
+
f"Proceed to upload {len(secrets_to_sync)} secrets? (yes/no): "
|
|
862
|
+
)
|
|
863
|
+
.strip()
|
|
864
|
+
.lower()
|
|
865
|
+
)
|
|
866
|
+
if confirm != "yes":
|
|
867
|
+
print("❌ Operation cancelled.")
|
|
868
|
+
sys.exit(0)
|
|
869
|
+
|
|
870
|
+
success_count = 0
|
|
871
|
+
for key, value in secrets_to_sync.items():
|
|
872
|
+
print(f" Uploading {key}...")
|
|
873
|
+
if manager.set(key, value):
|
|
874
|
+
success_count += 1
|
|
875
|
+
|
|
876
|
+
print(
|
|
877
|
+
f"\n✨ Successfully synced {success_count}/{len(secrets_to_sync)} secrets!"
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
except Exception as e:
|
|
881
|
+
print(f"❌ Error syncing secrets: {e}")
|
|
882
|
+
sys.exit(1)
|
|
883
|
+
|
|
805
884
|
else:
|
|
806
885
|
parser.print_help()
|
|
807
886
|
|
|
@@ -22,6 +22,8 @@ class PipelineGenerator:
|
|
|
22
22
|
health_url: Optional[str] = None,
|
|
23
23
|
backup_paths: Optional[list] = None,
|
|
24
24
|
app_port: int = 8000,
|
|
25
|
+
hydrate_env: bool = True,
|
|
26
|
+
combine_compose: bool = True,
|
|
25
27
|
):
|
|
26
28
|
self.platform = platform
|
|
27
29
|
self.strategy = strategy
|
|
@@ -37,6 +39,8 @@ class PipelineGenerator:
|
|
|
37
39
|
)
|
|
38
40
|
self.backup_paths = backup_paths or ["/app", "/etc/nginx"]
|
|
39
41
|
self.app_port = app_port
|
|
42
|
+
self.hydrate_env = hydrate_env
|
|
43
|
+
self.combine_compose = combine_compose
|
|
40
44
|
|
|
41
45
|
# New options
|
|
42
46
|
self.run_linting = False
|
|
@@ -50,8 +54,7 @@ class PipelineGenerator:
|
|
|
50
54
|
run_tests: bool = False,
|
|
51
55
|
python_version: str = "3.11",
|
|
52
56
|
branch: str = "main",
|
|
53
|
-
):
|
|
54
|
-
"""Set additional options for the pipeline."""
|
|
57
|
+
): """Set additional options for the pipeline."""
|
|
55
58
|
self.run_linting = run_linting
|
|
56
59
|
self.run_tests = run_tests
|
|
57
60
|
self.python_version = python_version
|
|
@@ -120,6 +123,12 @@ class PipelineGenerator:
|
|
|
120
123
|
if self.enable_dry_run:
|
|
121
124
|
steps.append(self._step_dry_run())
|
|
122
125
|
|
|
126
|
+
if self.hydrate_env:
|
|
127
|
+
steps.append(self._step_hydrate_env())
|
|
128
|
+
|
|
129
|
+
if self.combine_compose:
|
|
130
|
+
steps.append(self._step_combine_compose())
|
|
131
|
+
|
|
123
132
|
# Main deployment
|
|
124
133
|
steps.append(self._step_deploy())
|
|
125
134
|
|
|
@@ -143,7 +152,7 @@ class PipelineGenerator:
|
|
|
143
152
|
# Combine all steps
|
|
144
153
|
steps_yaml = "\n".join(steps)
|
|
145
154
|
|
|
146
|
-
return f"""name: Deploy
|
|
155
|
+
return f"""name: Deploy with vm_tool
|
|
147
156
|
|
|
148
157
|
on:
|
|
149
158
|
push:
|
|
@@ -153,8 +162,8 @@ on:
|
|
|
153
162
|
workflow_dispatch:
|
|
154
163
|
|
|
155
164
|
env:
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
SSH_HOSTNAME: ${{{{ secrets.SSH_HOSTNAME }}}}
|
|
166
|
+
SSH_USERNAME: ${{{{ secrets.SSH_USERNAME }}}}
|
|
158
167
|
APP_PORT: {self.app_port}
|
|
159
168
|
|
|
160
169
|
jobs:
|
|
@@ -176,16 +185,16 @@ jobs:
|
|
|
176
185
|
echo "🔐 Validating GitHub Secrets..."
|
|
177
186
|
MISSING_SECRETS=()
|
|
178
187
|
|
|
179
|
-
if [ -z "${{ secrets.
|
|
180
|
-
MISSING_SECRETS+=("
|
|
188
|
+
if [ -z "${{ secrets.SSH_HOSTNAME }}" ]; then
|
|
189
|
+
MISSING_SECRETS+=("SSH_HOSTNAME")
|
|
181
190
|
fi
|
|
182
191
|
|
|
183
|
-
if [ -z "${{ secrets.
|
|
184
|
-
MISSING_SECRETS+=("
|
|
192
|
+
if [ -z "${{ secrets.SSH_USERNAME }}" ]; then
|
|
193
|
+
MISSING_SECRETS+=("SSH_USERNAME")
|
|
185
194
|
fi
|
|
186
195
|
|
|
187
|
-
if [ -z "${{ secrets.
|
|
188
|
-
MISSING_SECRETS+=("
|
|
196
|
+
if [ -z "${{ secrets.SSH_ID_RSA }}" ]; then
|
|
197
|
+
MISSING_SECRETS+=("SSH_ID_RSA")
|
|
189
198
|
fi
|
|
190
199
|
|
|
191
200
|
if [ ${#MISSING_SECRETS[@]} -ne 0 ]; then
|
|
@@ -199,16 +208,16 @@ jobs:
|
|
|
199
208
|
echo "2. Add each secret:"
|
|
200
209
|
echo ""
|
|
201
210
|
|
|
202
|
-
if [[ " ${MISSING_SECRETS[*]} " =~ "
|
|
203
|
-
echo "
|
|
211
|
+
if [[ " ${MISSING_SECRETS[*]} " =~ " SSH_HOSTNAME " ]]; then
|
|
212
|
+
echo " SSH_HOSTNAME: Your Server IP (e.g., 54.123.45.67)"
|
|
204
213
|
fi
|
|
205
214
|
|
|
206
|
-
if [[ " ${MISSING_SECRETS[*]} " =~ "
|
|
207
|
-
echo "
|
|
215
|
+
if [[ " ${MISSING_SECRETS[*]} " =~ " SSH_USERNAME " ]]; then
|
|
216
|
+
echo " SSH_USERNAME: SSH username (e.g., ubuntu)"
|
|
208
217
|
fi
|
|
209
218
|
|
|
210
|
-
if [[ " ${MISSING_SECRETS[*]} " =~ "
|
|
211
|
-
echo "
|
|
219
|
+
if [[ " ${MISSING_SECRETS[*]} " =~ " SSH_ID_RSA " ]]; then
|
|
220
|
+
echo " SSH_ID_RSA: Run 'cat ~/.ssh/id_rsa' and copy output"
|
|
212
221
|
fi
|
|
213
222
|
|
|
214
223
|
echo ""
|
|
@@ -293,9 +302,54 @@ jobs:
|
|
|
293
302
|
- name: Set up SSH
|
|
294
303
|
run: |
|
|
295
304
|
mkdir -p ~/.ssh
|
|
296
|
-
echo "${{ secrets.
|
|
305
|
+
echo "${{ secrets.SSH_ID_RSA }}" > ~/.ssh/deploy_key
|
|
297
306
|
chmod 600 ~/.ssh/deploy_key
|
|
298
|
-
ssh-keyscan -H ${{ secrets.
|
|
307
|
+
ssh-keyscan -H ${{ secrets.SSH_HOSTNAME }} >> ~/.ssh/known_hosts
|
|
308
|
+
|
|
309
|
+
# Create SSH config to use the key for the specific host
|
|
310
|
+
cat > ~/.ssh/config <<EOF
|
|
311
|
+
Host ${{ secrets.SSH_HOSTNAME }}
|
|
312
|
+
User ${{ secrets.SSH_USERNAME }}
|
|
313
|
+
IdentityFile ~/.ssh/deploy_key
|
|
314
|
+
StrictHostKeyChecking no
|
|
315
|
+
EOF"""
|
|
316
|
+
|
|
317
|
+
def _step_hydrate_env(self) -> str:
|
|
318
|
+
return """
|
|
319
|
+
- name: Hydrate Environment Files from Secrets
|
|
320
|
+
env:
|
|
321
|
+
SECRETS_CONTEXT: ${{ toJSON(secrets) }}
|
|
322
|
+
run: |
|
|
323
|
+
# Use vm_tool to create local env files from secrets based on compose file
|
|
324
|
+
vm_tool hydrate-env \\
|
|
325
|
+
--compose-file docker-compose.yml \\
|
|
326
|
+
--secrets "$SECRETS_CONTEXT"
|
|
327
|
+
echo "✅ Hydrated environment files from secrets"
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def _step_combine_compose(self) -> str:
|
|
331
|
+
return """
|
|
332
|
+
- name: Combine Docker Compose files
|
|
333
|
+
run: |
|
|
334
|
+
# Combine base and prod compose files if prod exists
|
|
335
|
+
if [ -f docker/docker-compose.prod.yml ]; then
|
|
336
|
+
echo "Merging docker-compose.yml and docker/docker-compose.prod.yml..."
|
|
337
|
+
docker compose -f docker-compose.yml -f docker/docker-compose.prod.yml config > docker-compose.released.yml
|
|
338
|
+
elif [ -f docker-compose.prod.yml ]; then
|
|
339
|
+
echo "Merging docker-compose.yml and docker-compose.prod.yml..."
|
|
340
|
+
docker compose -f docker-compose.yml -f docker-compose.prod.yml config > docker-compose.released.yml
|
|
341
|
+
else
|
|
342
|
+
echo "No prod overriding file found, using base file..."
|
|
343
|
+
docker compose -f docker-compose.yml config > docker-compose.released.yml
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# Replace absolute paths (from CI runner) with relative paths
|
|
347
|
+
# This assumes the structure: /home/runner/work/repo/repo/ -> ./
|
|
348
|
+
sed -i 's|/home/runner/work/[^/]*/[^/]*||g' docker-compose.released.yml
|
|
349
|
+
|
|
350
|
+
echo "✅ Generated combined Docker Compose file"
|
|
351
|
+
cat docker-compose.released.yml
|
|
352
|
+
"""
|
|
299
353
|
|
|
300
354
|
def _step_validate_ssh(self) -> str:
|
|
301
355
|
return """
|
|
@@ -356,25 +410,52 @@ jobs:
|
|
|
356
410
|
def _step_deploy(self) -> str:
|
|
357
411
|
return """
|
|
358
412
|
- name: Deploy with vm_tool (Ansible-based)
|
|
413
|
+
env:
|
|
414
|
+
# Define deployment command for the released file
|
|
415
|
+
DEPLOY_COMMAND: "docker compose -f docker-compose.released.yml up -d --remove-orphans"
|
|
359
416
|
run: |
|
|
360
417
|
# Create inventory file for Ansible
|
|
361
418
|
cat > inventory.yml << EOF
|
|
362
419
|
all:
|
|
363
420
|
hosts:
|
|
364
421
|
production:
|
|
365
|
-
ansible_host: ${{ secrets.
|
|
366
|
-
ansible_user: ${{ secrets.
|
|
422
|
+
ansible_host: ${{ secrets.SSH_HOSTNAME }}
|
|
423
|
+
ansible_user: ${{ secrets.SSH_USERNAME }}
|
|
367
424
|
ansible_ssh_private_key_file: ~/.ssh/deploy_key
|
|
368
425
|
EOF
|
|
369
426
|
|
|
370
427
|
# Deploy using vm_tool (uses Ansible under the hood)
|
|
371
428
|
export GITHUB_REPOSITORY_OWNER=${{ github.repository_owner }}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
429
|
+
|
|
430
|
+
# Determine Project Directory based on user (handle root case)
|
|
431
|
+
if [ "${{ secrets.SSH_USERNAME }}" = "root" ]; then
|
|
432
|
+
HOME_DIR="/root"
|
|
433
|
+
else
|
|
434
|
+
HOME_DIR="/home/${{ secrets.SSH_USERNAME }}"
|
|
435
|
+
fi
|
|
436
|
+
PROJECT_DIR="${HOME_DIR}/apps/${{ github.event.repository.name }}"
|
|
437
|
+
|
|
438
|
+
# Prepare arguments
|
|
439
|
+
ARGS=(
|
|
440
|
+
"--inventory" "inventory.yml"
|
|
441
|
+
"--compose-file" "docker-compose.released.yml"
|
|
442
|
+
"--project-dir" "$PROJECT_DIR"
|
|
443
|
+
"--deploy-command" "${{ env.DEPLOY_COMMAND }}"
|
|
444
|
+
"--force"
|
|
445
|
+
"--health-url" "http://${{ secrets.SSH_HOSTNAME }}:${{ env.APP_PORT }}/health"
|
|
446
|
+
"--host" "${{ secrets.SSH_HOSTNAME }}"
|
|
447
|
+
"--user" "${{ secrets.SSH_USERNAME }}"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Move .env if it exists and add to args
|
|
451
|
+
if [ -f .env ]; then
|
|
452
|
+
mkdir -p env
|
|
453
|
+
mv .env env/.env
|
|
454
|
+
ARGS+=("--env-file" "env/.env")
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
vm_tool deploy-docker "${ARGS[@]}"
|
|
458
|
+
"""
|
|
378
459
|
|
|
379
460
|
def _step_health_check(self) -> str:
|
|
380
461
|
return f"""
|
|
@@ -394,10 +475,13 @@ jobs:
|
|
|
394
475
|
return """
|
|
395
476
|
- name: Verify
|
|
396
477
|
run: |
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
478
|
+
# Use absolute path for verify step too
|
|
479
|
+
PROJECT_DIR="/home/${{ secrets.SSH_USERNAME }}/apps/${{ github.event.repository.name }}"
|
|
480
|
+
|
|
481
|
+
ssh -i ~/.ssh/deploy_key ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOSTNAME }} << EOF
|
|
482
|
+
cd $PROJECT_DIR
|
|
483
|
+
docker compose ps
|
|
484
|
+
docker compose logs --tail=20
|
|
401
485
|
EOF"""
|
|
402
486
|
|
|
403
487
|
def _step_rollback(self) -> str:
|
|
@@ -406,12 +490,12 @@ jobs:
|
|
|
406
490
|
if: failure()
|
|
407
491
|
run: |
|
|
408
492
|
echo "⚠️ Rolling back..."
|
|
409
|
-
|
|
493
|
+
# Note: Rollback logic might need adjustment for absolute paths, keeping simple for now
|
|
494
|
+
ssh -i ~/.ssh/deploy_key ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOSTNAME }} << 'EOF'
|
|
410
495
|
BACKUP=$(ls -t ~/backups/*.tar.gz 2>/dev/null | head -1)
|
|
411
496
|
if [ -n "$BACKUP" ]; then
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
echo "✅ Rolled back"
|
|
497
|
+
# TODO: Improve rollback to handle dynamic dirs
|
|
498
|
+
echo "Rollback not fully implemented for dynamic paths yet"
|
|
415
499
|
fi
|
|
416
500
|
EOF"""
|
|
417
501
|
|
|
@@ -420,7 +504,7 @@ jobs:
|
|
|
420
504
|
- name: Cleanup
|
|
421
505
|
if: success()
|
|
422
506
|
run: |
|
|
423
|
-
ssh -i ~/.ssh/deploy_key ${{ secrets.
|
|
507
|
+
ssh -i ~/.ssh/deploy_key ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOSTNAME }} << 'EOF'
|
|
424
508
|
cd ~/backups 2>/dev/null || exit 0
|
|
425
509
|
ls -t *.tar.gz 2>/dev/null | tail -n +6 | xargs rm -f || true
|
|
426
510
|
EOF"""
|
|
@@ -431,7 +515,7 @@ jobs:
|
|
|
431
515
|
if: always()
|
|
432
516
|
run: |
|
|
433
517
|
if [ "${{ job.status }}" == "success" ]; then
|
|
434
|
-
echo "✅ Deployed to ${{ secrets.
|
|
518
|
+
echo "✅ Deployed to ${{ secrets.SSH_HOSTNAME }}:${{ env.APP_PORT }}"
|
|
435
519
|
else
|
|
436
520
|
echo "❌ Deployment failed"
|
|
437
521
|
fi"""
|
|
@@ -246,6 +246,76 @@ class EncryptedFileBackend(SecretsBackend):
|
|
|
246
246
|
return False
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
class GitHubSecretsBackend(SecretsBackend):
|
|
250
|
+
"""GitHub Actions Secrets backend using 'gh' CLI."""
|
|
251
|
+
|
|
252
|
+
def __init__(self, repo: Optional[str] = None):
|
|
253
|
+
import shutil
|
|
254
|
+
|
|
255
|
+
self.gh_cli = shutil.which("gh")
|
|
256
|
+
if not self.gh_cli:
|
|
257
|
+
raise RuntimeError("GitHub CLI ('gh') is not installed.")
|
|
258
|
+
self.repo = repo
|
|
259
|
+
|
|
260
|
+
def get_secret(self, key: str) -> Optional[str]:
|
|
261
|
+
"""
|
|
262
|
+
Get secret from GitHub.
|
|
263
|
+
Note: GitHub Secrets are write-only. We can't retrieve the value.
|
|
264
|
+
"""
|
|
265
|
+
logger.warning("Cannot retrieve secret value from GitHub (write-only).")
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
def set_secret(self, key: str, value: str) -> bool:
|
|
269
|
+
"""Set secret in GitHub Actions."""
|
|
270
|
+
import subprocess
|
|
271
|
+
|
|
272
|
+
cmd = ["gh", "secret", "set", key]
|
|
273
|
+
if self.repo:
|
|
274
|
+
cmd.extend(["--repo", self.repo])
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
# Pass value via stdin
|
|
278
|
+
subprocess.run(cmd, input=value, check=True, text=True, capture_output=True)
|
|
279
|
+
logger.info(f"Secret '{key}' stored in GitHub Secrets")
|
|
280
|
+
return True
|
|
281
|
+
except subprocess.CalledProcessError as e:
|
|
282
|
+
logger.error(f"Failed to set secret in GitHub: {e.stderr.strip()}")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
def delete_secret(self, key: str) -> bool:
|
|
286
|
+
"""Delete secret from GitHub Actions."""
|
|
287
|
+
import subprocess
|
|
288
|
+
|
|
289
|
+
cmd = ["gh", "secret", "delete", key]
|
|
290
|
+
if self.repo:
|
|
291
|
+
cmd.extend(["--repo", self.repo])
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
subprocess.run(cmd, check=True, text=True, capture_output=True)
|
|
295
|
+
logger.info(f"Secret '{key}' deleted from GitHub Secrets")
|
|
296
|
+
return True
|
|
297
|
+
except subprocess.CalledProcessError as e:
|
|
298
|
+
logger.error(f"Failed to delete secret from GitHub: {e.stderr.strip()}")
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def list_secrets(self) -> list:
|
|
302
|
+
"""List all secret keys in GitHub Actions."""
|
|
303
|
+
import subprocess
|
|
304
|
+
import json
|
|
305
|
+
|
|
306
|
+
cmd = ["gh", "secret", "list", "--json", "name"]
|
|
307
|
+
if self.repo:
|
|
308
|
+
cmd.extend(["--repo", self.repo])
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
result = subprocess.run(cmd, check=True, text=True, capture_output=True)
|
|
312
|
+
secrets = json.loads(result.stdout)
|
|
313
|
+
return [s["name"] for s in secrets]
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Failed to list secrets from GitHub: {e}")
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
|
|
249
319
|
class SecretsManager:
|
|
250
320
|
"""Unified secrets manager supporting multiple backends."""
|
|
251
321
|
|
|
@@ -283,3 +353,8 @@ class SecretsManager:
|
|
|
283
353
|
def from_file(cls, secrets_file: str = ".secrets.enc", **kwargs):
|
|
284
354
|
"""Create secrets manager with encrypted file backend."""
|
|
285
355
|
return cls(EncryptedFileBackend(secrets_file, **kwargs))
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def from_github(cls, repo: Optional[str] = None):
|
|
359
|
+
"""Create secrets manager with GitHub backend."""
|
|
360
|
+
return cls(GitHubSecretsBackend(repo))
|
|
@@ -7,12 +7,15 @@ MANIFEST.in
|
|
|
7
7
|
Makefile
|
|
8
8
|
README.md
|
|
9
9
|
codePushToGithub.py
|
|
10
|
+
jingo.code-workspace
|
|
10
11
|
mkdocs.yml
|
|
11
12
|
pyproject.toml
|
|
12
13
|
requirements-docs.txt
|
|
13
14
|
requirements.txt
|
|
14
15
|
runtime.txt
|
|
15
16
|
setup.py
|
|
17
|
+
.agent/workflows/push.md
|
|
18
|
+
.agent/workflows/test_and_lint.md
|
|
16
19
|
.devcontainer/devcontainer.json
|
|
17
20
|
.github/dependabot.yml
|
|
18
21
|
.github/workflows/ci.yml
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|