git » homepage.git » commit 349900e

Validate the manual caller of reusable deployment jobs

author Alan Dipert
2026-11-11 18:09:24 UTC
committer Alan Dipert
2026-11-11 18:09:24 UTC
parent c05cd69ca1b454c5e0736ab4f59a9f9fb587125a

Validate the manual caller of reusable deployment jobs

Forgejo reports workflow_call inside reusable jobs so the runner can bind inputs. Validate the current run through the API instead: its original trigger must be workflow_dispatch, its workflow deploy.yml, and its SHA and branch the requested master revision. Retain newest-successful ci.yml verification and cover invalid caller identities in regression tests.

Confirmed against Forgejo 16.0.6 services/actions/context.go, GenerateGiteaContext, and the live parent run: https://codeberg.org/forgejo/forgejo/src/tag/v16.0.6/services/actions/context.go

tests/test_deploy.py +16 -8
tools/deploy.py +8 -2

diff --git a/tests/test_deploy.py b/tests/test_deploy.py
index c983618..6ed9e4d 100644
--- a/tests/test_deploy.py
+++ b/tests/test_deploy.py
@@ -12,12 +12,15 @@ class DeployTests(unittest.TestCase):
     def setUp(self):
         self.enterContext(patch.dict(os.environ, {
             'GITHUB_REPOSITORY': 'alan/homepage', 'GITHUB_SHA': 'a' * 40,
-            'GITHUB_REF': 'refs/heads/master', 'GITHUB_EVENT_NAME': 'workflow_dispatch',
+            'GITHUB_REF': 'refs/heads/master', 'GITHUB_EVENT_NAME': 'workflow_call',
+            'GITHUB_RUN_ID': '99',
             'CHECK_TOKEN': 'check-fixture', 'PUBLISH_TOKEN': 'publish-fixture',
             'SITE_DEPLOY_KEY': 'key-fixture'}))
+        self.invocation = {'workflow_id': 'deploy.yml', 'trigger_event': 'workflow_dispatch',
+                           'commit_sha': 'a' * 40, 'prettyref': 'master'}
         self.ci_run = {'id': 42, 'status': 'success', 'commit_sha': 'a' * 40,
-                    'prettyref': 'master', 'workflow_id': 'ci.yml',
-                    'event': '', 'trigger_event': 'workflow_dispatch'}
+                       'prettyref': 'master', 'workflow_id': 'ci.yml',
+                       'event': '', 'trigger_event': 'workflow_dispatch'}
 
     def test_requires_the_latest_successful_master_check(self):
         for change in ({}, {'status': 'failure'}, {'status': 'running'}, {'status': 'waiting'},
@@ -25,7 +28,7 @@ class DeployTests(unittest.TestCase):
                        {'status': 'unknown'}, {'commit_sha': 'b' * 40}, {'prettyref': 'branch'},
                        {'workflow_id': 'deploy.yml'}, {'trigger_event': 'pull_request'}):
             with self.subTest(change=change), patch('deploy.api', side_effect=[
-                    {'commit': {'id': 'a' * 40}},
+                    {'commit': {'id': 'a' * 40}}, self.invocation,
                     {'workflow_runs': [self.ci_run | change, self.ci_run]}]):
                 if change:
                     with self.assertRaises(ValueError):
@@ -33,15 +36,20 @@ class DeployTests(unittest.TestCase):
                 else:
                     self.assertEqual(deploy.checked_source(), 42)
         for responses in ([{'commit': {'id': 'b' * 40}}],
-                          [{'commit': {'id': 'a' * 40}}, {'workflow_runs': []}]):
+                          [{'commit': {'id': 'a' * 40}}, self.invocation, {'workflow_runs': []}]):
             with self.subTest(responses=responses), patch('deploy.api', side_effect=responses):
                 with self.assertRaises(ValueError):
                     deploy.checked_source()
-        for change in ({'GITHUB_REF': 'refs/heads/branch'}, {'GITHUB_EVENT_NAME': 'push'}):
-            with patch.dict(os.environ, change), patch('deploy.api') as api:
+        for change in ({'trigger_event': 'push'}, {'workflow_id': 'ci.yml'},
+                       {'commit_sha': 'b' * 40}, {'prettyref': 'branch'}):
+            with self.subTest(invocation=change), patch('deploy.api', side_effect=[
+                    {'commit': {'id': 'a' * 40}}, self.invocation | change]):
                 with self.assertRaises(ValueError):
                     deploy.checked_source()
-                api.assert_not_called()
+        with patch.dict(os.environ, {'GITHUB_REF': 'refs/heads/branch'}), patch('deploy.api') as api:
+            with self.assertRaises(ValueError):
+                deploy.checked_source()
+            api.assert_not_called()
 
     def test_dry_run_and_deploy_keep_credentials_private_and_order_effects(self):
         for dry_run in (True, False):
diff --git a/tools/deploy.py b/tools/deploy.py
index 3915f70..07073e3 100644
--- a/tools/deploy.py
+++ b/tools/deploy.py
@@ -23,11 +23,17 @@ def api(path, token, body=None):
 def checked_source():
     repository, commit, ref = (os.environ[key] for key in
                                ('GITHUB_REPOSITORY', 'GITHUB_SHA', 'GITHUB_REF'))
-    if ref != 'refs/heads/master' or os.environ['GITHUB_EVENT_NAME'] != 'workflow_dispatch':
-        raise ValueError('Deploy must be run manually on master')
+    if ref != 'refs/heads/master':
+        raise ValueError('Deploy must run on master')
     token = os.environ['CHECK_TOKEN']
     if api(repository + '/branches/master', token)['commit']['id'] != commit:
         raise ValueError('master moved; start a new deployment')
+    # A reused job reports workflow_call; the API retains the caller's trigger.
+    invocation = api(repository + '/actions/runs/' + os.environ['GITHUB_RUN_ID'], token)
+    if ((invocation['workflow_id'], invocation['trigger_event'],
+         invocation['commit_sha'], invocation['prettyref']) !=
+            ('deploy.yml', 'workflow_dispatch', commit, 'master')):
+        raise ValueError('Deploy must be run manually on master')
     query = urllib.parse.urlencode({'head_sha': commit, 'ref': ref,
                                    'workflow_id': 'ci.yml', 'limit': 1})
     runs = api(repository + '/actions/runs?' + query, token)['workflow_runs']