Skip to content

fix(ci): resolve deploy environment from inputs, not github.event_name - #1809

Merged
cristim merged 1 commit into
mainfrom
fix/1805-reusable-workflow-environment
Aug 12, 2026
Merged

cristim merged 1 commit into
mainfrom
fix/1805-reusable-workflow-environment

Conversation

@cristim

@cristim cristim commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

Closes #1805

The defect

Inside a reusable workflow github.event_name is the caller's event, never the literal string workflow_call. Four deploy workflows branched on it:

case "$EVENT_NAME" in
  workflow_dispatch|workflow_call) ENVIRONMENT="$INPUT_ENVIRONMENT" ;;
  *)                               ENVIRONMENT=dev ;;
esac

deploy-all.yml triggers on release: types: [created], computes environment=prod, and passes it to every callee. On that path the callee sees event_name=release, matches neither arm of the first case, discards the prod it was handed, and returns dev. The workflow_dispatch path worked only by coincidence: there the caller's event genuinely is workflow_dispatch, which the arm happens to match. That is why the bug is invisible on the path anyone would test with.

The signal, and why this is not "add release to the case arm"

Adding release treats the symptom and stays fragile: the next trigger added to deploy-all.yml reintroduces it silently. github.event_name is the wrong signal entirely, because it always describes the triggering event of the whole run and can never answer "was I called by another workflow".

The fix branches on whether an environment was requested - inputs.environment - which answers exactly that question:

  • inputs is populated on precisely the two triggers that carry an environment (workflow_dispatch, workflow_call) and is empty on every other trigger, whatever the caller's event was.
  • environment is already required: true on both of those triggers in all four workflows, so an empty value means none was requested.
  • Where an empty value has no defensible default, the step now fails rather than guessing. A deploy that cannot determine its target environment must stop, not pick one.

In-repo precedent: database-migration.yml already resolves its environment straight from inputs.environment, and its comment at :107-110 documents that an input absent on a given path "renders empty".

Which workflows shared the pattern

Workflow Status before Change
deploy-azure.yml broken - release path returned dev branch on input; dev only for its own push trigger; everything else fails
deploy-gcp.yml broken - release path returned dev same
deploy-aws-fargate.yml broken - release path returned dev declares no trigger but workflow_dispatch/workflow_call, so the input is the only source of truth and the fallback is removed outright
deploy-aws-lambda.yml correct result, wrong reasoning had an explicit release) prod arm, so it hardcoded the answer instead of honouring the input. The input now wins; the arm serves only its own direct release trigger
deploy-all.yml caller, not callee github.event_name is sound here (this workflow is always the entry point of its own run) and stays. Gains the dev|staging|prod allowlist the callees already had
database-migration.yml already correct none - reads inputs.environment directly

deploy-all.yml gaining the allowlist is what closes the last way to reach a wrong environment through a workflow_call: it is the only caller of these workflows, and an empty or unknown environment now stops the fan-out instead of reaching four deployments. It also stops interpolating ${{ inputs.environment }} directly into a shell command.

That allowlist makes determine-deployment the first job in this workflow that can deliberately fail, which surfaced a second, related defect. notify runs if: always() and its Check for failures step only tested the four deploy jobs for == "failure". When determine-deployment stops the run those jobs are skipped, not failed, so the step summary would print "✅ All deployments completed successfully!" for a run that deployed nothing. That is the same shape as #1805 itself, a summary asserting something the deployment did not do, and this PR made it reachable, so it is fixed here: the step now treats any non-success of determine-deployment as a failure.

A sibling of that gap - a cancelled run also reaching the success line - was deliberately not fixed here and is split out to #1810. It is unchanged from origin/main, so this PR did not cause it, and it is a class issue across all four deploy workflows' summary jobs; fixing one instance here would leave three live. Check for failures carries a comment pointing at LeanerCloud/cloud-commitments-platform#196 rather than staying silent about it. The full analysis, including the documented proof that the obvious fix is safe, is on that issue.

Blast radius correction

Issue #1805 originally said a release-triggered run "silently targets dev on three clouds". It is two: deploy-all.yml:105 hardcodes deploy-aws-fargate=false, so a release fans out to Lambda, GCP and Azure only, and Lambda was already resolving to prod. The consequence would be Azure and GCP applying Terraform against dev while the run summary reported prod. Fargate was latently broken and is fixed here too.

Tense matters here: nothing has actually been mis-deployed. gh run list --workflow deploy-all.yml returns [] - the workflow has never executed, so the release path has never fired. The bug is real by inspection and would fire on the first release; it has no history. The impact claim on #1805 has since been retracted by its author on the strength of this, and the correction is posted there.

The comment this PR makes false, and CodeRabbit's stale learning

The deploy-azure: job in deploy-all.yml documented that the called workflow's prepare output "is not necessarily the environment computed above, so do not assume the two agree". That was correct, and correct precisely because of this bug. CodeRabbit challenged it on #1803, the challenge was declined with the trace, and CodeRabbit agreed the finding was invalid and stored a learning to that effect.

This PR inverts that. prepare now takes the passed input verbatim or fails the run, so the two do agree, and the comment is updated in the same commit. CodeRabbit therefore holds a now-stale learning from #1803 about this exact line. Reviewers should not treat it as authoritative here.

Does #1803's concurrency reasoning still hold

Yes, and it was re-checked rather than assumed. deploy-azure.yml's job-level group azure-tfstate-${{ needs.prepare.outputs.environment }} and the state key github-${ENVIRONMENT}.terraform.tfstate both derive from the same prepare output, so they stay in lockstep whatever prepare decides. This change alters only what that output evaluates to, never the fact that both sides read it. The comment at deploy-azure.yml:130-132 ("prepare rejects anything outside dev|staging|prod and fails the run, so this expression cannot evaluate to an empty suffix") remains true and is now reinforced, since an empty input fails earlier as well.

Verification

What was proved. The Determine environment / Set environment step body was extracted from the YAML for both origin/main and this branch (parsed, not retyped), the ${{ }} expressions substituted with the values each scenario would present, and executed under bash with a scratch $GITHUB_OUTPUT.

Release path, event_name=release + inputs.environment=prod:

Workflow before after
deploy-azure.yml environment=dev environment=prod
deploy-gcp.yml environment=dev environment=prod
deploy-aws-fargate.yml environment=dev environment=prod
deploy-aws-lambda.yml target_environment=prod target_environment=prod

The negative, a workflow_call that passes no environment (event_name=release + inputs.environment=''):

Workflow before after
deploy-azure.yml environment=dev fails, ::error::No environment supplied on a 'release'-triggered run; refusing to guess a deployment target.
deploy-gcp.yml environment=dev fails, same
deploy-aws-fargate.yml environment=dev fails, ::error::Refusing unknown environment: ''

Unchanged where it should be: direct push to main still resolves dev on Azure, GCP and Lambda; direct release on Lambda still resolves prod; workflow_dispatch still honours its choice on all four.

What was NOT proved. No release was created and no run was executed. gh run list --workflow deploy-all.yml is empty, so the release path has never run at all. This is a standalone simulation of the resolution logic, not an end-to-end deploy: it does not prove that the resolved prod reaches terraform apply against the prod state file, only that prepare now emits prod where it previously emitted dev. Everything downstream already reads needs.prepare.outputs.* and is unchanged by this PR.

Two residuals, stated rather than hidden. Note that required: true on a workflow_call input requires the key to be present, not the value to be non-empty, so a caller can deliver an empty input:

  • A future caller triggered by push passing an empty environment would resolve dev on Azure/GCP, because github.event_name is inherited and reads as push there too.
  • A future caller triggered by release passing an empty environment would resolve prod on Lambda, because Lambda's own release trigger legitimately means prod and is indistinguishable from an inherited one.

Neither is reachable: deploy-all.yml is the only caller, it has neither a push trigger nor a path to an empty environment now that it allowlists the value it passes. Telling a direct trigger from an inherited one would need github.job_workflow_ref, which is not worth the machinery for an unreachable path. Both residuals are documented in the comments at the relevant branch rather than papered over.

actionlint (v1.7.12) is not wired into CI, so the same command was run against origin/main for comparison:

actionlint .github/workflows/deploy-all.yml .github/workflows/deploy-azure.yml \
           .github/workflows/deploy-gcp.yml .github/workflows/deploy-aws-fargate.yml \
           .github/workflows/deploy-aws-lambda.yml

Exit 1 on both origin/main and this branch - entirely pre-existing shellcheck SC2086/SC2129 debt in untouched steps. Findings go 113 -> 111: no new findings, none on any line this PR touches, and the two that disappear are pre-existing unquoted $GITHUB_OUTPUT uses in the deploy-all.yml step that was rewritten.

A note on counts, for whoever picks up LeanerCloud/cloud-commitments-platform#196

Worth recording, because LeanerCloud/cloud-commitments-platform#196's actionable content is a count and this PR is the cautionary tale.

Four count errors occurred in this one changeset, in a PR whose entire subject is code asserting something the codebase does not do:

  1. The original deploy-all.yml summary reports success on a cancelled run (only success-by-default check in the repo) cloud-commitments-platform#196 filing said the cancelled-run bug was "a class across four workflows". It is one instance.
  2. The correction to that said "three siblings allowlist on success". It is four - deploy-gcp.yml:307 was missing.
  3. An independent verification reproduced error 2, because it re-derived each listed entry's polarity correctly but inherited the membership of the set from the list it was given.
  4. The sweep backing "the only denylist in the repo" was described as covering "all 17 workflows". There are 16 .yml files; the 17th directory entry is README.md.

Every one came from counting the shape that was salient - files containing if: always(), files someone happened to enumerate, entries in a directory listing - rather than enumerating the candidate set and then measuring the defining property on each member. Note that error 3 survived a review that was correct on every substantive point, which is the dangerous part: re-deriving one side of a comparison while accepting the other side as given is not verification.

The generalisable rule: a count in a code comment or a filed issue is a claim about the whole repository, and the only thing that makes it true is enumerating every candidate and reading what each one does. Never carry a count forward from a previous message, a previous revision, or someone else's list, including a reviewer's.

The substantive claims survived all four corrections unchanged: deploy-all.yml is the only success-by-default check in the repo, and the cancelled-run gap is a single site. Only the numbers were ever wrong, which is precisely why they were easy to miss.

Two smaller things

All five allowlist errors now quote the rejected value ('$ENVIRONMENT'). Unquoted, an empty or whitespace value renders as Refusing unknown environment: with nothing after it. This matches the existing style in database-migration.yml:127.

deploy-aws-lambda.yml's Set image tag step still branches on github.event_name and was deliberately left alone. It is not the same bug class: the environment is a deployment intent the caller communicates out-of-band through inputs, and the caller's event is a bad proxy for it, whereas the image tag is a property of the code being deployed, which a called workflow genuinely inherits along with the ref it checks out. On a release-triggered caller, github.event.release.tag_name is the release actually being deployed, which is the correct answer. All six trigger combinations were enumerated and it is right in each; its empty-tag path already fails loudly.

@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/internal Team-internal only effort/s Hours type/bug Defect labels Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88f56b7f-40c4-4c1c-a011-74ccb5199f37

📥 Commits

Reviewing files that changed from the base of the PR and between dd19342 and eb49e5a.

📒 Files selected for processing (1)
  • .github/workflows/deploy-all.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/deploy-all.yml

📝 Walkthrough

Walkthrough

Deployment workflows now preserve explicit environment inputs, map supported events to environments, validate dev, staging, and prod, and fail instead of silently defaulting unsupported contexts to dev.

Changes

Deployment environment handling

Layer / File(s) Summary
Caller environment resolution and failure reporting
.github/workflows/deploy-all.yml
deploy-all.yml resolves and validates the environment, passes it to called workflows without substitution, and treats unsuccessful environment determination as a deployment failure.
Reusable workflow environment handling
.github/workflows/deploy-aws-lambda.yml, .github/workflows/deploy-aws-fargate.yml, .github/workflows/deploy-azure.yml, .github/workflows/deploy-gcp.yml
Cloud workflows prioritize supplied inputs, apply only supported event defaults, and fail when the environment is missing or invalid.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: ⚪ Minimal · up to eb49e

The workflows now resolve deployment environments from explicit inputs and fail instead of guessing when no valid target is supplied, preventing incorrect environment selection; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant CallerWorkflow
  participant DetermineDeployment
  participant CloudWorkflow
  CallerWorkflow->>DetermineDeployment: resolve environment from input or event
  DetermineDeployment->>DetermineDeployment: validate dev, staging, or prod
  DetermineDeployment->>CloudWorkflow: pass validated environment
  DetermineDeployment-->>CallerWorkflow: report failure when determination is unsuccessful
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: resolving deployment environments from workflow inputs instead of github.event_name.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1805-reusable-workflow-environment

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy-all.yml:
- Around line 70-84: Update the ENVIRONMENT selection in the deploy workflow so
push events map to dev before falling back to INPUT_ENVIRONMENT; preserve
release-to-prod mapping and validation for other events.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c4293f0-470b-4126-aa38-b81c3473fc6a

📥 Commits

Reviewing files that changed from the base of the PR and between 8b16ed4 and 7ba01a6.

📒 Files selected for processing (5)
  • .github/workflows/deploy-all.yml
  • .github/workflows/deploy-aws-fargate.yml
  • .github/workflows/deploy-aws-lambda.yml
  • .github/workflows/deploy-azure.yml
  • .github/workflows/deploy-gcp.yml

Comment thread .github/workflows/deploy-all.yml
@cristim
cristim force-pushed the fix/1805-reusable-workflow-environment branch from 7ba01a6 to c80634e Compare August 12, 2026 21:21
@cristim

cristim commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Pre-merge adversarial review round (independent reviewer, findings folded into c80634e)

Recording this on the PR rather than leaving it in an agent transcript. The first pushed head (7ba01a6) was reviewed by an independent reviewer instructed not to trust the implementer. Eight findings; all actionable ones are fixed in the amended head.

Fixed

# Sev Finding Fix
F1 med notify / Check for failures only tested the four deploy jobs for == "failure". The new allowlist makes determine-deployment the first job that can deliberately fail, and in that case the deploy jobs are skipped, not failed, so the summary would print "✅ All deployments completed successfully!" for a run that deployed nothing. Same shape as #1805 itself. Condition now includes needs.determine-deployment.result != "success"; the failure line reworded to cover a run that stopped before deploying
F2 med The Azure/GCP comment claimed "only a direct push is allowed to imply one". False along exactly the axis this PR fixes: EVENT_NAME is push for a direct push and for a workflow_call from a push-triggered caller. Also required: true on a workflow_call input requires the key, not a non-empty value, so "therefore means none was requested" did not follow. Both comments rewritten to state the rule and name the inherited-push residual explicitly
F2b med deploy-aws-lambda.yml still carried the un-softened version of the same claim Same treatment
F4 low deploy-aws-fargate.yml's comment described an event that cannot have occurred: deploy-all.yml:105 hardcodes deploy-aws-fargate=false, so it has never called Fargate Hedged to "would have mis-resolved"; notes the case was latent there and live only for Azure and GCP
F6 nit Rejected-value quoting applied to only 2 of 5 allowlists All five now quote
F7 nit The updated deploy-azure: comment documented its own revision history Dropped; git does that

Accepted as documented residuals, not fixed (F3, F5). Because a caller can deliver an empty value to a required: true input, two paths remain where a future caller could get a wrong environment: an inherited push resolving dev on Azure/GCP, and an inherited release resolving prod on Lambda. Both are unreachable today (deploy-all.yml is the only caller, has no push trigger, and now allowlists what it passes). Separating a direct trigger from an inherited one would need github.job_workflow_ref; that is disproportionate machinery for an unreachable path, so the residuals are written into the comments at the relevant branch instead of denied. deploy-all.yml's own allowlist is likewise unreachable today (its workflow_dispatch input is a server-validated type: choice) and is kept deliberately as the thing that makes the above unreachable.

F8, tense. gh run list --workflow deploy-all.yml returns []. deploy-all.yml has never executed, so the release path has never fired and nothing has been mis-deployed. The commit message and PR body were corrected from past to conditional.

Independently confirmed, no action

  • ${{ inputs.environment }} renders empty, not the literal "null", when the workflow is triggered by push/release. The inputs context is available in steps.env with no event qualifier, so this is the documented "dereferencing a nonexistent property evaluates to an empty string" case, and Null casts to String as ''. The in-repo ${{ inputs.deploy_to || 'all' }} idiom at deploy-all.yml:92 is only correct under the same rule. Had this been wrong the failure would have been loud and immediate (allowlist rejection on the next push to main), not silent.
  • deploy-aws-lambda.yml's Set image tag is not the same bug class; all six trigger combinations enumerated and correct. Reasoning is in the PR body.
  • No regressions: every declared trigger of all five workflows walked against origin/main; each path gives the same or a strictly better answer.
  • Nothing missed: the workflow_call-callable set is exactly {azure, gcp, fargate, lambda, database-migration}; database-migration.yml already reads inputs.* directly and re-validates against the same allowlist.
  • A property worth noting: the new code is correct even if its own premise were wrong. If github.event_name somehow were workflow_call inside a called workflow, the non-empty input still wins first. The old code was load-bearing on that premise; this is not.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy-all.yml:
- Around line 294-298: Update the deployment-result condition in the summary job
to compare each selected deployment’s result against success rather than
checking only for failure. Ensure cancelled selected jobs are treated as
unsuccessful while skipped, unselected jobs do not trigger the condition; apply
this to deploy-aws-lambda, deploy-aws-fargate, deploy-gcp, and deploy-azure
alongside determine-deployment.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23c7149f-0979-457d-b460-63fa705fbebd

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba01a6 and c80634e.

📒 Files selected for processing (5)
  • .github/workflows/deploy-all.yml
  • .github/workflows/deploy-aws-fargate.yml
  • .github/workflows/deploy-aws-lambda.yml
  • .github/workflows/deploy-azure.yml
  • .github/workflows/deploy-gcp.yml
🚧 Files skipped from review as they are similar to previous changes (4)
  • .github/workflows/deploy-azure.yml
  • .github/workflows/deploy-aws-fargate.yml
  • .github/workflows/deploy-gcp.yml
  • .github/workflows/deploy-aws-lambda.yml

Comment on lines +294 to 298
if [[ "${{ needs.determine-deployment.result }}" != "success" ]] || \
[[ "${{ needs.deploy-aws-lambda.result }}" == "failure" ]] || \
[[ "${{ needs.deploy-aws-fargate.result }}" == "failure" ]] || \
[[ "${{ needs.deploy-gcp.result }}" == "failure" ]] || \
[[ "${{ needs.deploy-azure.result }}" == "failure" ]]; then

@coderabbitai coderabbitai Bot Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '286,309p' .github/workflows/deploy-all.yml

# Validate in GitHub Actions with a selected reusable deployment job that exits
# as cancelled. Expected: the `notify` job fails and does not emit the success
# summary.

Repository: LeanerCloud/CUDly

Length of output: 1342


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- deployment job declarations and result aggregation ---'
sed -n '1,120p' .github/workflows/deploy-all.yml
sed -n '120,220p' .github/workflows/deploy-all.yml
sed -n '250,315p' .github/workflows/deploy-all.yml

printf '%s\n' '--- all references to deployment selectors and job conditions ---'
rg -n -C 3 'deploy-(aws-lambda|aws-fargate|gcp|azure)|determine-deployment|needs\..*\.result|if:' .github/workflows/deploy-all.yml

Repository: LeanerCloud/CUDly

Length of output: 20448


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from itertools import product

selectors = {
    "aws-lambda": {"all", "aws-only", "aws-gcp", "aws-azure"},
    "aws-fargate": set(),
    "gcp": {"all", "gcp-only", "aws-gcp", "gcp-azure"},
    "azure": {"all", "azure-only", "aws-azure", "gcp-azure"},
}
strategies = ["all", "aws-only", "gcp-only", "azure-only", "aws-gcp", "aws-azure", "gcp-azure"]
results = ["success", "failure", "cancelled", "skipped"]

for strategy in strategies:
    selected = [name for name, enabled in selectors.items() if strategy in enabled]
    false_positive = []
    for statuses in product(results, repeat=len(selected)):
        current = all(status != "failure" for status in statuses)
        proposed = all(status == "success" for status in statuses)
        if current and not proposed:
            false_positive.append(dict(zip(selected, statuses)))
    print(strategy, "selected=", selected, "current-success-but-not-proposed=", false_positive[:8],
          "count=", len(false_positive))
PY

printf '%s\n' '--- reusable workflow job-level conditions and cancellation-sensitive constructs ---'
for file in $(fd -i 'deploy-(aws-lambda|aws-fargate|gcp|azure)\.yml$' .github/workflows); do
  echo "### $file"
  rg -n -C 2 '^[[:space:]]+(if:|needs:|concurrency:)|cancel-in-progress|always\(\)|failure\(\)|cancelled\(\)' "$file" || true
done

Repository: LeanerCloud/CUDly

Length of output: 7179


🌐 Web query:

GitHub Actions documentation needs context result values cancelled skipped reusable workflow caller job result

💡 Result:

In GitHub Actions, you can determine the result of a job in a reusable workflow using the jobs context, which is available within the reusable workflow itself [1][2]. The jobs.<job_id>.result property provides the status of a specific job and can return one of the following string values: success, failure, cancelled, or skipped [1][2]. Regarding the context in a caller workflow: 1. Caller Workflow Limitations: When a reusable workflow is triggered, the caller workflow treats the entire job that calls the reusable workflow as a single unit [3]. You can access the outputs of the reusable workflow, but the individual internal job statuses (like whether a specific internal job was cancelled or skipped) are not automatically exposed as properties in the caller workflow's jobs context [1][4]. 2. Handling Skipped or Cancelled States: If you need to trigger logic in a caller workflow based on the specific outcome of an internal job within a reusable workflow, you must explicitly define outputs in the reusable workflow [3][4]. You can set these outputs to reflect the result of internal jobs using expressions like ${{ jobs.<job_id>.result }} [1][4]. 3. Concurrency and Cancellation: If you use concurrency with cancel-in-progress: true in both the caller and the called reusable workflow, ensure you are not using the same concurrency group name, as this may lead to the caller workflow being prematurely cancelled [5][6]. To cancel sub-workflows, you can apply concurrency settings directly within the reusable workflow definition [7]. For debugging purposes, you can inspect the available information by printing the context to the logs using ${{ toJSON(jobs) }} within the reusable workflow [4].

Citations:


Treat cancelled selected deployments as unsuccessful.

The current check flags only failure, so a selected job with result cancelled can produce a success summary. Compare each selected deployment result with success; keep unselected jobs as skipped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-all.yml around lines 294 - 298, Update the
deployment-result condition in the summary job to compare each selected
deployment’s result against success rather than checking only for failure.
Ensure cancelled selected jobs are treated as unsuccessful while skipped,
unselected jobs do not trigger the condition; apply this to deploy-aws-lambda,
deploy-aws-fargate, deploy-gcp, and deploy-azure alongside determine-deployment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid finding, and already tracked as #1810. Deliberately out of scope for this PR.

You are right about the substance: the Check for failures step tests each deploy job for == "failure", and a cancelled job is not failure, so a run killed mid-apply prints "All deployments completed successfully!". Converting to an allowlist on == "success" is the correct shape.

It is excluded here for one reason: this PR did not cause it. The four == "failure" tests are byte-identical to origin/main. The related change that was folded into this PR is the determine-deployment case, and that one qualified precisely because the new environment allowlist made that job able to fail for the first time, turning previously-impossible skipped states into reachable ones.

Some context recorded on #1810 that is worth having before anyone picks it up, since two of these were wrong when first written:

  • It is one site, not a class. == "failure" appears 4 times in deploy-all.yml and zero times in any other workflow. Enumerated across all 16 workflow files, every other .result comparison allowlists on success, including all four per-cloud summary jobs. deploy-all.yml is the outlier, not the pattern.
  • In-repo precedent exists for the fix shape: ci.yml:740-744 already tests contains(needs.*.result, 'failure') and contains(needs.*.result, 'cancelled') as separate states.
  • The four per-cloud summary jobs do print "Deployment failed" on a cancelled run. Wrong wording, safe direction, tracked separately on #1810 so it does not get conflated with this fix.

Leaving the code unchanged here; the gap is documented in-place with a pointer to #1810.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cristim, thanks for the detailed clarification.

The cancellation gap is valid, but it predates this PR and is tracked in #1810. The added determine-deployment.result != "success" check addresses the new failure path introduced by this PR. Keeping the four existing deployment-result checks unchanged is appropriate for this PR’s scope.


✏️ Learnings added
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 1809
File: .github/workflows/deploy-all.yml:313-317
Timestamp: 2026-08-12T21:59:41.967Z
Learning: In `.github/workflows/deploy-all.yml`, the `notify` job checks the four deployment job results with `== "failure"`. A selected deployment with result `cancelled` can therefore reach the success summary. This pre-existing gap is tracked in GitHub issue `#1810` and is deliberately out of scope for PR `#1809`. The PR-specific fix is the `needs.determine-deployment.result != "success"` check because PR `#1809` introduces environment validation that can cause `determine-deployment` to fail and downstream jobs to be skipped.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 1809
File: .github/workflows/deploy-all.yml:79-93
Timestamp: 2026-08-12T21:59:15.382Z
Learning: In `.github/workflows/deploy-all.yml`, only `workflow_dispatch` and `release` trigger the workflow. `release` resolves to `prod`, and `workflow_dispatch` requires an allowlisted `dev`, `staging`, or `prod` environment. The `Set environment` allowlist must fail for an empty or unsupported environment rather than map a potential future `push` event to `dev`, so that a new trigger cannot silently choose a deployment environment.

You are interacting with an AI system.

@cristim

cristim commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Round 2 adversarial review (head dd19342)

All eight round-1 findings re-verified as fixed. Five new items. Three taken, one declined and split out to LeanerCloud/cloud-commitments-platform#196, one declined as not earning its change.

Taken

# Sev Finding Fix
R2-F2 low The Azure/GCP comments described the deleted *) default arm in the present tense ("lands on the default arm and resolves to dev"), sitting directly above code that has no default arm. An artefact of the round-1 tense correction, and precisely the failure mode this PR exists to prevent Conditional, matching the Fargate hedge: "would have landed on a dev default arm and resolved to dev while the summary said prod"
R2-F4 nit deploy-all.yml: "prepare takes the input it is passed and fails the run rather than substituting a default" is false unqualified. Azure's prepare does substitute dev on the push+empty arm. True only on the path the comment is about One word: "takes the non-empty input it is passed"
R2-F3 low Three files' comments now rest on three facts about deploy-all.yml (only caller, no push trigger, allowlists what it passes). Adding a push: trigger here would silently falsify all three and restore #1805's failure mode, but the warning lived only in the files that rely on the fact, not where the change would be made Warning added at deploy-all.yml's on: block, where that edit would happen. In scope: this coupling did not exist before this PR, since the callees previously ignored inputs entirely

Declined and split out: R2-F1 → LeanerCloud/cloud-commitments-platform#196

The finding is real. notify runs if: always() and tests the four deploy jobs for == "failure". On a cancelled run, determine-deployment.result is success and the deploy jobs are cancelled, so no clause matches and the summary prints "✅ All deployments completed successfully!" for a run killed mid-apply. That is worse than cosmetic given deploy-azure.yml already names that scenario as the worst outcome available.

It is not this PR's to fix. The four == "failure" tests are unchanged from origin/main, and the cancellation path behaved identically before this change. Contrast the adjacent determine-deployment case, which this PR did fold in: that one only became reachable because adding the allowlist made determine-deployment able to fail for the first time. Caused-by-this-PR is the line, and this falls the other side of it.

It is also a class issue across four workflows — deploy-all.yml plus deploy-azure.yml:453, deploy-aws-lambda.yml:441, deploy-aws-fargate.yml:293 all share the pattern. Fixing one instance inside a PR about environment resolution would half-fix a class and leave three live.

I did implement if: ${{ !cancelled() }}, then reverted it. Filed as #1810 with the full analysis, so the follow-up is short rather than re-derived: the documented rule chain proving !cancelled() bypasses the skipped-need auto-skip (load-bearing, because deploy-aws-fargate is permanently skipped and getting this wrong means notify silently never runs again), the YAML tag-indicator trap that makes ${{ }} mandatory, and a completeness check confirming run-level cancellation is the only producer of cancelled in this chain. Check for failures carries a comment pointing at LeanerCloud/cloud-commitments-platform#196 rather than staying silent about the gap.

Declined outright

R2-NIT — deploy-aws-lambda.yml names release and push as the two legitimate causes of an empty input, then documents the inheritance residual for release only. Adding the push case would dilute the emphasis on the genuinely dangerous one: an inherited release resolves prod, an inherited push resolves dev, and the identical dev residual is already spelled out in full in Azure and GCP. The reviewer ranked it ignorable; I agree, and I was not otherwise touching that paragraph.

Re-verified unchanged this round

dev unreachable from any workflow_call; no regressions on any declared trigger of any of the five workflows; Fargate's EVENT_NAME removal orphans nothing; no callable workflow missed; no newline-injection surface into $GITHUB_OUTPUT. actionlint 113 → 111 versus origin/main, no new findings, none on a touched line. pre-commit clean on all five files.

Also confirmed for the record, since the reviewer chased it independently: the Fargate hedge is exactly right and non-obviously so. Under the old Lambda code a release-triggered deploy-all.yml hit release) TARGET_ENVIRONMENT=prod and got prod, which is what deploy-all.yml computed anyway — so Lambda coincidentally resolved correctly and the bug was never live there either. "Live only for Azure and GCP" holds.

@cristim
cristim force-pushed the fix/1805-reusable-workflow-environment branch from dd19342 to eb49e5a Compare August 12, 2026 21:39
@cristim

cristim commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Round 3: a false claim I introduced, now corrected

Reviewer caught that the comment I added at Check for failures asserted something untrue about the codebase, in a PR whose entire theme is comments asserting things that are not true. Third time this class of defect has been caught in this review (R2-F2, R2-F4, now this), which is itself worth noting.

The false claim. I wrote that the cancelled-run gap was "a class issue across all four deploy workflows' summary jobs". I verified all three siblings and none of them has this bug:

File Line Condition Behaviour on cancelled
deploy-azure.yml 482 == "success" && == "success" falls to failure branch, safe
deploy-aws-lambda.yml 487 = "success" && = "success" falls to failure branch, safe
deploy-aws-fargate.yml 322 == "success" && == "success" falls to failure branch, safe

All three are allowlists on success. deploy-all.yml's Check for failures is the only denylist (== "failure") in the repo, and a denylist defaults to green on every state its author did not enumerate. That polarity difference is the bug, and it is a single site.

Why this needed correcting rather than shrugging off. Whoever picked up LeanerCloud/cloud-commitments-platform#196 would have opened three files hunting a bug that is not there, and the plausible outcome is "fixing" three correct summary jobs - churn on the deploy path for no gain. It also inverted the effort estimate: it read as a four-workflow sweep, it is one if: on one job.

Corrected in three places:

Also folded into LeanerCloud/cloud-commitments-platform#196 as an explicitly separate, lower-priority task: those three siblings do print "Deployment failed" on a cancelled run, which is wrong in wording but safe in direction. Its fix is a distinct third cancelled state in those conditionals, not the !cancelled() swap. Flagged so the two do not get merged into one job.

The environment-resolution change itself is unaffected - no logic changed in this round, comment only. actionlint still 111 vs 113 on origin/main; pre-commit clean. Head is now eb49e5a. Rounds 1-3 are closed from the reviewer's side with no further findings on the fix.

Inside a reusable workflow `github.event_name` is the caller's event and is
never the literal `workflow_call`, so the `workflow_dispatch|workflow_call`
arm in the called deploy workflows is dead code. A release-triggered
deploy-all.yml computes environment=prod and passes it to every cloud; each
callee then falls through to the `*)` arm and returns dev. Azure and GCP would
apply Terraform against dev while the run summary reported prod. Nothing has
been mis-deployed yet: deploy-all.yml has never run. The workflow_dispatch
path survives only by coincidence, the caller's event there genuinely being
workflow_dispatch.

Branch on whether an environment was requested instead. `inputs` is populated
on workflow_dispatch and workflow_call, the only two triggers that carry an
environment, so a non-empty input identifies a workflow_call invocation that
no event name can. Where an empty input has no defensible default the step now
fails rather than guessing a deployment target.

deploy-aws-fargate.yml declares no trigger besides workflow_dispatch and
workflow_call, so it loses its fallback outright. deploy-aws-lambda.yml
already produced the right answer but hardcoded it in a `release) prod` arm
instead of honouring the input; the input now wins and that arm serves only
its own direct release trigger.

deploy-all.yml is always the entry point of its own run, so its use of
github.event_name is sound and stays. It gains the dev|staging|prod allowlist
the callees already had, so an empty or unknown environment stops the fan-out
rather than reaching four deployments, and it no longer interpolates
inputs.environment straight into a shell command. Because that allowlist makes
determine-deployment the first job that can deliberately fail, the notify job
now treats any non-success there as a failure: the four deploy jobs are
skipped rather than failed in that case, so the summary would otherwise
announce that all deployments completed successfully for a run that deployed
nothing.

The comment on the deploy-azure job, which documented that the callee's
prepare output need not agree with the environment computed above, was true
only because of this bug and is corrected here.

Closes #1805
@cristim
cristim force-pushed the fix/1805-reusable-workflow-environment branch from eb49e5a to 3b73caa Compare August 12, 2026 21:43
@cristim

cristim commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Round 4: sibling count was three, is four

Correcting my own round-3 comment above, which listed a three-row table. deploy-gcp.yml has a summary: job at :274 (if: always() at :278) with the same allowlist at :307. The corrected set:

File Line Condition
deploy-azure.yml 482 == "success" && == "success"
deploy-gcp.yml 307 == "success" && == "success"
deploy-aws-lambda.yml 487 = "success" && = "success"
deploy-aws-fargate.yml 322 == "success" && == "success"

The three-row table in round 3 said "None of the three has this bug. Do not 'fix' them" while leaving GCP unlisted - which made it the one file a reader might then have gone and "fixed". Same direction as the defect it was correcting. Fixed in the workflow comment ("all four per-cloud summary jobs"), in LeanerCloud/cloud-commitments-platform#196's body and table, and recorded in a correction comment there.

Substantive claims re-verified by enumeration, not sampling:

  • Denylist sweep across all 16 workflow files: the only hits are deploy-all.yml:314-317. "The only success-by-default check in the repo" holds repo-wide.
  • Every .result comparison in the repo classified. Four per-cloud summary allowlists, four per-cloud status lines in Aggregate results (also allowlists, safe), the job-level always() && ... == 'success' gates, and rollback.yml:592-600. One denylist.
  • Bonus for deploy-all.yml summary reports success on a cancelled run (only success-by-default check in the repo) cloud-commitments-platform#196: ci.yml:740-744 already handles cancellation explicitly with a contains(needs.*.result, 'cancelled') test separate from its failure test. In-repo precedent for treating cancelled as its own state.

A note on the pattern, now added to the PR body since LeanerCloud/cloud-commitments-platform#196's actionable content is a count: there were four count errors in this changeset - "class across four workflows" (one), "three siblings" (four), an independent check that reproduced the second, and "17 workflows" (16, the 17th entry is README.md). Each came from counting a salient shape rather than enumerating the set and measuring the defining property. The third is the instructive one: it survived a review that was correct on every substantive point, because re-deriving one side of a comparison while accepting the other as given is not verification.

Every substantive claim survived all four corrections unchanged. Only the numbers were wrong, which is exactly why they were easy to miss.

Comment-only again - no logic moved. actionlint 111 vs origin/main's 113, re-measured this round against a fresh origin/main (still 8b16ed4c2). Head is 3b73caa29a22c7b42aedb7596865c40cc9e79124.

@cristim

cristim commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

CI status correction: CI - Build & Test has been failing, and I reported it as merely "in progress"

Correcting my own status reports. I checked this job several times, saw in_progress, and reported CI as green-except-one-still-running. It had in fact failed on three consecutive heads. I never waited for a terminal state before characterising it, which is exactly the optimistic-status error I should not make - and it is the same shape as the count errors above: asserting from a partial reading rather than from the finished measurement.

Diagnosis: runner network, not this change

Head Failing jobs Actual error
7ba01a67 Build Docker Image curl exit code 56 - failure in receiving network data
c80634ed Build Docker Image same step
dd19342d Build Docker Image, Lint Code truncated download -> sha256sum: 1 of 1 computed checksums did NOT match; Lint: Error: socket hang up

CI Success is a gate job and is red only because those are.

The two manifestations are one root cause. curl 56 is an outright receive failure; the checksum mismatch is the same fetch failing after writing a partial file, so the hash of a truncated tarball naturally does not match. The same builds also log WARNING: opening from cache https://dl-cdn.alpinelinux.org/... APKINDEX.tar.gz: No such file or directory, and the Lint failure is socket hang up. Every failure in every run is a network read.

Evidence it is not this PR

  1. The pin is not stale. I downloaded migrate.linux-amd64.tar.gz v4.19.1 and computed 2ac648fbd1b127b69ab5a7b33cf96212178f71e22379fc50573630c6f4c7ce18 - byte-exact match to the Dockerfile's pinned amd64 SHA. Upstream has not republished; nothing is actually wrong with the checksum.
  2. Diff scope. git diff origin/main --name-only filtered for anything outside .github/workflows/deploy-* returns empty. Five files, all deploy workflows. CI - Build & Test does not consume any of them - there is no mechanism by which this change reaches a Go lint run or a Docker build.
  3. main is green, including this branch's base 8b16ed4c2.

The honest caveat

Three consecutive failures is more than I would expect from independent flake, so this may be a sustained upstream/runner incident rather than random noise. I cannot fully separate the two: no other branch has run since main at 08:01Z, ~13 hours before my first run, so there is no contemporaneous control. What I can say is that the failure mode is environmental in kind and branch-independent in mechanism.

I am not touching CI to make this pass - an environmental fetch failure is a re-run situation, not debt to paper over. Two runs (eb49e5a, 3b73caa) are in flight; a watcher is armed on the head and will report every terminal conclusion. If they fail the same way this needs a re-run or an infra look, not a code change here.

@cristim

cristim commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Resolved: CI - Build & Test passed on eb49e5a with no code change

This supplies the control I said was missing. All five pushed heads carry identical Go code and an identical Dockerfile - every commit on this branch is confined to the five deploy-*.yml files, and the last two rounds were comment-only. The same build inputs failed three times and then passed on the fourth with nothing changed that CI - Build & Test reads.

That rules out the sustained-incident reading being anything to do with this branch: it was a window of runner network trouble that has now cleared. No re-run was needed, no CI change was made, and nothing was papered over.

3b73caa (current head) is still in flight; the watcher is armed and will report its terminal conclusion.

@cristim
cristim merged commit 17a568f into main Aug 12, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/internal Team-internal only priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reusable deploy workflows read github.event_name as workflow_call, silently downgrading a release deploy to dev

1 participant