Skip to content

Commit 025932a

Browse files
authored
redis n procfiles (#358)
* dd some smol redis instance types * make Procfile specifiable also * ignore comments and empty lines in procfiles
1 parent d0fb960 commit 025932a

11 files changed

Lines changed: 158 additions & 10 deletions

File tree

cabotage/celery/tasks/build.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -446,22 +446,23 @@ def file_path(filename):
446446
f"{git_ref(image.application.github_repository, image.commit_sha)}"
447447
)
448448

449-
procfile_body = _fetch_github_file(
450-
image.application.github_repository,
451-
image.commit_sha,
452-
access_token=access_token,
453-
filename=file_path("Procfile.cabotage"),
454-
)
455-
if procfile_body is None:
449+
procfile_candidates = ["Procfile.cabotage", "Procfile"]
450+
if image.application.procfile_path:
451+
procfile_candidates = [image.application.procfile_path]
452+
453+
procfile_body = None
454+
for candidate in procfile_candidates:
456455
procfile_body = _fetch_github_file(
457456
image.application.github_repository,
458457
image.commit_sha,
459458
access_token=access_token,
460-
filename=file_path("Procfile"),
459+
filename=file_path(candidate),
461460
)
461+
if procfile_body is not None:
462+
break
462463
if procfile_body is None:
463464
raise BuildError(
464-
"No Procfile.cabotage or Procfile found in root of "
465+
"No Procfile found in "
465466
f"{git_ref(image.application.github_repository, image.commit_sha)}"
466467
)
467468

cabotage/client/templates/user/project_application_settings.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ <h2 class="text-sm font-semibold text-base-content">Source & Automation</h2>
6767
{{ render_field_compact(form.auto_deploy_branch, placeholder='main', label='Branch') }}
6868
{{ render_field_compact(form.subdirectory, placeholder='/', label='Subdirectory') }}
6969
{{ render_field_compact(form.dockerfile_path, placeholder='Dockerfile.cabotage', label='Dockerfile Path') }}
70+
{{ render_field_compact(form.procfile_path, placeholder='Procfile.cabotage', label='Procfile Path') }}
7071
</div>
7172
<div class="mt-2">
7273
{{ render_field_compact(form.branch_deploy_watch_paths, placeholder='src/**', label='Watch Paths', extra_class='auto-grow') }}

cabotage/server/audit_helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"process_pod_classes": "pod classes",
3939
"subdirectory": "subdirectory",
4040
"dockerfile_path": "Dockerfile path",
41+
"procfile_path": "Procfile path",
4142
"branch_deploy_watch_paths": "watch paths",
4243
}
4344

cabotage/server/models/projects.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ def active_application_environments(self):
554554
subdirectory: Mapped[str | None] = mapped_column(Text())
555555

556556
dockerfile_path: Mapped[str | None] = mapped_column(Text())
557+
procfile_path: Mapped[str | None] = mapped_column(Text())
557558
branch_deploy_watch_paths: Mapped[Any | None] = mapped_column(
558559
postgresql.JSONB(),
559560
)

cabotage/server/models/resources.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@
6666
}
6767

6868
redis_size_classes: dict[str, dict[str, dict[str, str]]] = {
69+
"cache.nano": {
70+
"cpu": {"requests": "33m", "limits": "66m"},
71+
"memory": {"requests": "64Mi", "limits": "64Mi"},
72+
},
73+
"cache.micro": {
74+
"cpu": {"requests": "66m", "limits": "132m"},
75+
"memory": {"requests": "128Mi", "limits": "128Mi"},
76+
},
6977
"cache.small": {
7078
"cpu": {"requests": "125m", "limits": "250m"},
7179
"memory": {"requests": "256Mi", "limits": "256Mi"},

cabotage/server/user/forms.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,14 @@ class EditApplicationSettingsForm(FlaskForm):
362362
(lambda x: x if x else None),
363363
],
364364
)
365+
procfile_path = StringField(
366+
"Procfile Path",
367+
description="Custom path to Procfile (e.g. deploy/Procfile), falls back to Procfile.cabotage then Procfile",
368+
filters=[
369+
(lambda x: x.strip() if (x and isinstance(x, str)) else x),
370+
(lambda x: x if x else None),
371+
],
372+
)
365373
branch_deploy_watch_paths = TextAreaField(
366374
"Watch Paths",
367375
description="Only deploy this app when files matching these patterns change. One .gitignore-style pattern per line (e.g. src/**, Dockerfile). Leave empty to always deploy.",

cabotage/utils/procfile.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ def _parse_procfile_line(line):
9393
def loads(content):
9494
"""Load a Procfile from a string."""
9595
lines = _group_lines(line for line in content.split("\n"))
96-
lines = [(i, _parse_procfile_line(line)) for i, line in lines if line.strip()]
96+
lines = [
97+
(i, _parse_procfile_line(line))
98+
for i, line in lines
99+
if line.strip() and not line.lstrip().startswith("#")
100+
]
97101
errors = []
98102
# Reject files with duplicate process types (no sane default).
99103
duplicates = _find_duplicates(((i, line[0]) for i, line in lines))
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""add configurable procfile_path to applications
2+
3+
Revision ID: 2b6f8e9621b8
4+
Revises: aa90a43a1737
5+
Create Date: 2026-04-23 17:48:57.059148
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "2b6f8e9621b8"
15+
down_revision = "aa90a43a1737"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
with op.batch_alter_table("project_applications", schema=None) as batch_op:
23+
batch_op.add_column(sa.Column("procfile_path", sa.Text(), nullable=True))
24+
25+
with op.batch_alter_table("project_applications_version", schema=None) as batch_op:
26+
batch_op.add_column(
27+
sa.Column("procfile_path", sa.Text(), autoincrement=False, nullable=True)
28+
)
29+
30+
# ### end Alembic commands ###
31+
32+
33+
def downgrade():
34+
# ### commands auto generated by Alembic - please adjust! ###
35+
with op.batch_alter_table("project_applications_version", schema=None) as batch_op:
36+
batch_op.drop_column("procfile_path")
37+
38+
with op.batch_alter_table("project_applications", schema=None) as batch_op:
39+
batch_op.drop_column("procfile_path")
40+
41+
# ### end Alembic commands ###

tests/test_audit_helpers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,21 @@ def test_app_edit_shows_changed_fields(self, db_session, application):
537537
fields = {c["field"] for c in result[entry.id]}
538538
assert "auto deploy branch" in fields
539539

540+
def test_app_edit_procfile_path_uses_human_label(self, db_session, application):
541+
self._create_activity(db_session, "create", application)
542+
db_session.commit()
543+
544+
application.procfile_path = "deploy/Procfile.web"
545+
db_session.flush()
546+
act = self._create_activity(db_session, "edit", application)
547+
db_session.commit()
548+
entry = self._audit_entry_for(db_session, act)
549+
550+
result = compute_audit_changes([entry])
551+
assert entry.id in result
552+
fields = {c["field"] for c in result[entry.id]}
553+
assert "Procfile path" in fields
554+
540555
def test_app_edit_no_op(self, db_session, application):
541556
self._create_activity(db_session, "create", application)
542557
db_session.commit()

tests/test_build.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,61 @@ def test_missing_procfile_raises(
356356
with pytest.raises(BuildError, match="No Procfile"):
357357
build_image_buildkit(image)
358358

359+
@patch("cabotage.celery.tasks.build._fetch_github_file")
360+
def test_fetch_image_source_uses_custom_procfile_path(
361+
self,
362+
mock_fetch_file,
363+
app,
364+
db_session,
365+
image,
366+
):
367+
"""A configured procfile_path is fetched instead of the default candidates."""
368+
from cabotage.celery.tasks.build import _fetch_image_source
369+
370+
image.application.subdirectory = "services/api"
371+
image.application.procfile_path = "deploy/Procfile.web"
372+
mock_fetch_file.side_effect = [
373+
None,
374+
DOCKERFILE_BODY,
375+
PROCFILE_BODY,
376+
]
377+
378+
result = _fetch_image_source(image, access_token=None)
379+
380+
assert result["procfile_body"] == PROCFILE_BODY
381+
assert [call.kwargs["filename"] for call in mock_fetch_file.call_args_list] == [
382+
"services/api/Dockerfile.cabotage",
383+
"services/api/Dockerfile",
384+
"services/api/deploy/Procfile.web",
385+
]
386+
387+
@patch("cabotage.celery.tasks.build._fetch_github_file")
388+
def test_fetch_image_source_custom_procfile_path_does_not_fallback(
389+
self,
390+
mock_fetch_file,
391+
app,
392+
db_session,
393+
image,
394+
):
395+
"""A configured procfile_path mirrors dockerfile_path and disables fallback."""
396+
from cabotage.celery.tasks.build import _fetch_image_source, BuildError
397+
398+
image.application.procfile_path = "deploy/Procfile.web"
399+
mock_fetch_file.side_effect = [
400+
None,
401+
DOCKERFILE_BODY,
402+
None,
403+
]
404+
405+
with pytest.raises(BuildError, match="No Procfile"):
406+
_fetch_image_source(image, access_token=None)
407+
408+
assert [call.kwargs["filename"] for call in mock_fetch_file.call_args_list] == [
409+
"Dockerfile.cabotage",
410+
"Dockerfile",
411+
"deploy/Procfile.web",
412+
]
413+
359414
@patch("cabotage.celery.tasks.build.GithubIntegration")
360415
@patch("cabotage.celery.tasks.build.GithubAppAuth")
361416
@patch("cabotage.celery.tasks.build.github_app")

0 commit comments

Comments
 (0)