Skip to content

Commit 1fb4cdd

Browse files
authored
Document cpflow upstream testing (#746)
1 parent 0f27782 commit 1fb4cdd

5 files changed

Lines changed: 232 additions & 78 deletions

File tree

.controlplane/docs/testing-cpflow-github-actions.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,73 @@ workflow change was tested before a new `cpflow` gem release existed. That is
6060
safer than pinning a branch, but it should be treated as a temporary test pin
6161
until the next upstream release tag is available.
6262

63+
What is tied to the upstream repo:
64+
65+
- The downstream workflow wrapper hardcodes `shakacode/control-plane-flow` in
66+
`uses:`.
67+
- The reusable workflow file comes from the ref after `@`.
68+
- The reusable workflow checks out `control-plane-flow` at
69+
`control_plane_flow_ref` to load shared composite actions.
70+
- When `CPFLOW_VERSION` is empty, the setup action builds and installs the
71+
`cpflow` gem from that checked-out repository ref.
72+
73+
What is tied to RubyGems:
74+
75+
- A released `cpflow` gem is the normal source used to generate downstream
76+
workflow wrappers.
77+
- The `CPFLOW_VERSION` repository variable is a runtime override that runs
78+
`gem install cpflow -v <version>` inside workflows.
79+
80+
For stable downstream automation, prefer the release path: generate from a
81+
released gem and pin wrappers to the matching upstream release tag. For
82+
pre-release validation, pin to a full commit SHA from the upstream PR, never a
83+
moving branch.
84+
85+
## Testing An Unmerged Upstream PR Downstream
86+
87+
You can test an upstream `control-plane-flow` PR in this downstream app before
88+
merging upstream, without publishing a gem. Use an immutable commit SHA from the
89+
upstream PR branch:
90+
91+
1. Push the upstream PR branch and copy its head commit SHA.
92+
2. In a downstream test branch, pin every generated wrapper ref:
93+
94+
```sh
95+
bin/pin-cpflow-github-ref <upstream-pr-sha>
96+
```
97+
98+
The helper accepts release tags and full 40-character commit SHAs by default.
99+
It rejects branch names such as `main` or `feature/foo`; use
100+
`--allow-moving-ref` only for short-lived local experiments that will not be
101+
committed. The resulting diff should replace both pins in each reusable
102+
workflow call:
103+
104+
```yaml
105+
uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@<upstream-pr-sha>
106+
with:
107+
control_plane_flow_ref: <upstream-pr-sha>
108+
```
109+
110+
3. Keep `CPFLOW_VERSION` unset unless you intentionally want to test a released
111+
RubyGems version instead of building `cpflow` from the upstream PR SHA.
112+
4. Run `bin/test-cpflow-github-flow`.
113+
5. Open a downstream PR and trigger a real review app with a comment whose body
114+
is exactly:
115+
116+
```text
117+
+review-app-deploy
118+
```
119+
120+
6. Verify the deploy logs show the expected upstream commit SHA, the setup step
121+
prints the expected `cpflow` version/source, and the review app URL returns
122+
HTTP 200.
123+
7. After the upstream PR merges and releases, regenerate or repin downstream to
124+
the release tag instead of leaving the temporary commit SHA forever.
125+
126+
This tests the real reusable workflow and shared composite actions from the
127+
upstream PR. It avoids merging upstream blind while also avoiding a mutable
128+
branch ref in downstream automation.
129+
63130
## Local Checks
64131

65132
After regenerating the flow, run these checks from the repository root. If
@@ -83,7 +150,10 @@ inside composite action metadata, including `description:` fields. Literal
83150
examples such as `${{ vars.SOME_VALUE }}` can fail action loading before any
84151
shell step starts. The wrapper runs `cpflow github-flow-readiness`, parses the
85152
generated YAML, checks action input descriptions for literal GitHub expressions,
86-
and runs `actionlint -ignore 'SC2129' .github/workflows/cpflow-*.yml`.
153+
checks that every generated wrapper keeps `uses:` and `control_plane_flow_ref`
154+
on the same upstream ref across all `cpflow-*` wrappers, checks that any
155+
secret-inheriting reusable workflow passes `control_plane_flow_ref`, and runs
156+
`actionlint -ignore 'SC2129' .github/workflows/cpflow-*.yml`.
87157

88158
## PR Checks
89159

@@ -168,6 +238,12 @@ Create the first review app by commenting exactly:
168238

169239
## Ways To Make This Easier
170240

241+
- Extend `bin/pin-cpflow-github-ref` so it can also run
242+
`bin/test-cpflow-github-flow`, open a downstream PR, and print or post the
243+
exact `+review-app-deploy` command needed to start the canary deploy.
244+
- Add CI coverage that runs `bin/test-cpflow-github-flow` on generated workflow
245+
changes, so ref mismatches and action metadata parsing issues are caught
246+
before review.
171247
- Add a no-secret GitHub Actions smoke workflow that loads generated local
172248
composite actions from the PR branch and fails fast on action metadata parsing.
173249
- Extend `bin/test-cpflow-github-flow` as more local cpflow GitHub Actions

.controlplane/readme.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,16 @@ setup action installs that RubyGems version instead. For normal release pins,
437437
either leave it unset while using the matching `v<version>` workflow tag, or set
438438
it to the same gem version without the leading `v`.
439439

440+
To test unreleased upstream workflow changes before merging `control-plane-flow`,
441+
pin a downstream PR to the upstream PR's full commit SHA in both `uses:` and
442+
`control_plane_flow_ref`, leave `CPFLOW_VERSION` unset, and trigger a real review
443+
app deploy. That tests the upstream reusable workflow, shared composite actions,
444+
and source-built `cpflow` gem from the same immutable commit. After the upstream
445+
change is released, regenerate or repin back to the matching release tag.
446+
Use `bin/pin-cpflow-github-ref <ref>` to update the generated wrapper refs
447+
together. The helper accepts release tags and full commit SHAs by default;
448+
`--allow-moving-ref` is only for short-lived local experiments.
449+
440450
For this app, validate a regenerated flow with:
441451

442452
```bash

.github/testing-github-actions.md

Lines changed: 26 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,34 @@
1-
# Developing and Testing Github Actions
1+
# Developing and Testing GitHub Actions
22

3-
Testing Github Actions on an existing repository is tricky.
4-
5-
The main issue boils down to the fact that Github Actions uses the workflow files in the branch where the event originates. This is fine for push events, but it becomes a problem when you want to test workflows that are triggered by comments on a pull request.
3+
GitHub Actions workflow testing depends on the event type:
64

7-
Here's a summary of the behavior:
8-
9-
Behavior of push and pull_request Events
10-
1. Push on a Branch:
11-
• When you push changes to a branch (e.g., feature-branch), GitHub Actions uses the workflow files in that same branch.
12-
• This is why changes to workflows work seamlessly when testing with push events.
13-
2. Pull Request Events:
14-
• For pull_request events (e.g., a PR from feature-branch into master), GitHub Actions will always use the workflow files from the target branch (e.g., master), not the source branch (e.g., feature-branch).
15-
• This is a security feature to prevent someone from introducing malicious code in a PR that modifies the workflow files themselves.
5+
- `push` runs workflow files from the pushed branch.
6+
- `pull_request` runs workflow files from the base branch for sensitive cases.
7+
- `issue_comment` runs workflow files from the default branch.
8+
- `workflow_dispatch --ref <branch>` runs the workflow file from that ref.
169

17-
Impact on Comment-Triggered Workflows
10+
This matters for review-app automation because comment-triggered commands such
11+
as `+review-app-deploy` use default-branch workflow code. A PR that changes
12+
workflow files or action wiring is not fully proven by commenting on that same
13+
PR until the trusted default-branch code has those changes.
1814

19-
When you want to trigger workflows via comments (issue_comment) in a pull request:
20-
• The workflow code used will always come from the master branch (or the default branch), regardless of the branch where the PR originates.
21-
• This means the PR’s changes to the workflow won’t be used, and the action invoked by the comment will also use code from master.
15+
For cpflow review-app, staging, and promotion workflows, use the cpflow-specific
16+
guide:
2217

23-
Workarounds to Test Comment-Triggered Workflows
18+
[Testing cpflow GitHub Actions Changes](../.controlplane/docs/testing-cpflow-github-actions.md)
2419

25-
If you want to test workflows in a way that uses the changes in the pull request, here are your options:
20+
## Practical Pattern
2621

27-
1. Use Push Events for Testing
28-
• Test your changes on a branch with push triggers.
29-
• Use workflow_dispatch to simulate the events you need (like invoking actions via comments).
22+
1. Validate generated files locally with `bin/test-cpflow-github-flow`.
23+
2. Open a PR and let regular CI prove GitHub can parse the workflow YAML.
24+
3. For top-level workflow-file experiments, run `workflow_dispatch --ref`.
25+
4. For comment-triggered review-app commands, test a real `+review-app-deploy`
26+
after the trusted default-branch wrapper points at the code under test.
27+
5. When testing unreleased upstream `control-plane-flow` changes downstream, pin
28+
both the reusable workflow `uses:` ref and `control_plane_flow_ref` to the
29+
same upstream commit SHA.
3030

31-
This allows you to confirm that your changes to the workflow file or actions behave as expected before merging into master.
32-
33-
2. Merge the Workflow to master Temporarily
34-
35-
If you absolutely need the workflow to run as part of a pull_request event:
36-
1. Merge your workflow changes into master temporarily.
37-
2. Open a PR to test your comment-triggered workflows.
38-
3. Revert the changes in master if necessary.
39-
40-
This ensures the workflow changes are active in master while still testing with the pull_request context.
41-
42-
3. Add Logic to Detect the Source Branch
43-
44-
Use github.event.pull_request.head.ref to add custom logic in your workflow that behaves differently based on the source branch.
45-
• Example:
46-
47-
jobs:
48-
test-pr:
49-
runs-on: ubuntu-latest
50-
if: ${{ github.event.pull_request.head.ref == 'feature-branch' }}
51-
steps:
52-
- name: Checkout Code
53-
uses: actions/checkout@v3
54-
55-
- name: Debug
56-
run: echo "Testing workflow changes in feature-branch"
57-
58-
However, this still requires the workflow itself to exist in master.
59-
60-
4. Use a Fork or a Temporary Repo
61-
62-
Create a temporary repository or a fork to test workflows in isolation:
63-
• Push your workflow changes to master in the test repository.
64-
• Open a PR in the fork to test how workflows behave with issue_comment events and PR contexts.
65-
66-
Once confirmed, you can replicate the changes in your main repository.
67-
68-
6. Alternative Approach: Split Workflows
69-
70-
If your workflow includes comment-based triggers (issue_comment), consider splitting your workflows:
71-
• A base workflow in master that handles triggering.
72-
• A test-specific workflow for validating changes on a branch.
73-
74-
For example:
75-
1. The base workflow triggers when a comment like /run-tests is added.
76-
2. The test-specific workflow runs in response to the base workflow but uses the branch’s code.
77-
78-
Summary
79-
• For push events: The branch-specific workflow is used, so testing changes is easy.
80-
• For pull_request and issue_comment events: GitHub always uses workflows from the master branch, and there’s no direct way to bypass this.
81-
82-
To test comment-triggered workflows:
83-
1. Use push or workflow_dispatch to validate changes.
84-
2. Merge workflow changes temporarily into master to test with pull_request events.
85-
3. Use tools like act for local simulation.
31+
Avoid testing production automation against moving branch refs such as `main` or
32+
a feature branch. Use release tags for normal operation and full commit SHAs for
33+
temporary pre-release validation. `bin/pin-cpflow-github-ref` enforces that
34+
default and requires `--allow-moving-ref` for one-off local branch experiments.

bin/pin-cpflow-github-ref

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "pathname"
5+
6+
USAGE = <<~USAGE
7+
Usage: bin/pin-cpflow-github-ref [--allow-moving-ref] <control-plane-flow-ref>
8+
9+
Use a release tag for normal operation, e.g. v5.0.0.
10+
Use a full 40-character commit SHA for temporary unreleased upstream testing.
11+
Use --allow-moving-ref only for short-lived local branch/ref experiments.
12+
USAGE
13+
14+
ALLOWED_OPTIONS = ["--allow-moving-ref"].freeze
15+
FULL_COMMIT_SHA = /\A[0-9a-f]{40}\z/i
16+
RELEASE_TAG = /\Av\d+\.\d+\.\d+(?:[-.][0-9A-Za-z][0-9A-Za-z.-]*)?\z/
17+
18+
options, positional = ARGV.partition { |arg| arg.start_with?("--") }
19+
unknown_options = options - ALLOWED_OPTIONS
20+
21+
unless unknown_options.empty?
22+
warn "Unknown option(s): #{unknown_options.join(', ')}"
23+
warn USAGE
24+
exit 1
25+
end
26+
27+
unless positional.length == 1
28+
warn USAGE
29+
exit 1
30+
end
31+
32+
ref = positional.first
33+
allow_moving_ref = options.include?("--allow-moving-ref")
34+
35+
unless ref.match?(/\A[0-9A-Za-z._\/-]+\z/)
36+
warn "Ref contains unsupported characters: #{ref.inspect}"
37+
exit 1
38+
end
39+
40+
unless ref.match?(FULL_COMMIT_SHA) || ref.match?(RELEASE_TAG) || allow_moving_ref
41+
warn "Ref must be a full 40-character commit SHA or a v-prefixed release tag: #{ref.inspect}"
42+
warn "Use --allow-moving-ref only for a short-lived branch/ref experiment."
43+
exit 1
44+
end
45+
46+
root = Pathname.new(__dir__).join("..").expand_path
47+
workflow_paths = Dir[root.join(".github/workflows/cpflow-*.yml")].sort
48+
49+
if workflow_paths.empty?
50+
warn "No cpflow workflow wrappers found."
51+
exit 1
52+
end
53+
54+
changed = []
55+
workflow_paths.each do |path|
56+
text = File.read(path)
57+
updated = text
58+
.gsub(%r{(uses:\s+shakacode/control-plane-flow/\.github/workflows/[^@\s]+@)[^\s]+}, "\\1#{ref}")
59+
.gsub(/(\bcontrol_plane_flow_ref:\s*)\S+/, "\\1#{ref}")
60+
61+
next if updated == text
62+
63+
File.write(path, updated)
64+
changed << Pathname.new(path).relative_path_from(root).to_s
65+
end
66+
67+
puts "Pinned cpflow GitHub ref to #{ref}"
68+
if changed.empty?
69+
puts "No files changed."
70+
else
71+
changed.each { |path| puts "updated #{path}" }
72+
end

bin/test-cpflow-github-flow

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,52 @@ abort bad.join("\n") unless bad.empty?
3838
puts "no action metadata descriptions contain GitHub expressions"
3939
RUBY
4040

41+
echo "==> check cpflow reusable workflow refs"
42+
bin/conductor-exec ruby <<'RUBY'
43+
require "yaml"
44+
45+
CONTROL_PLANE_FLOW_WORKFLOW = %r{\Ashakacode/control-plane-flow/\.github/workflows/[^@\s]+@([^\s]+)\z}
46+
47+
refs = Hash.new { |hash, key| hash[key] = [] }
48+
49+
Dir[".github/workflows/cpflow-*.yml"].sort.each do |path|
50+
doc = YAML.load_file(path, aliases: true)
51+
doc.fetch("jobs", {}).each do |job_name, job|
52+
next unless job.is_a?(Hash)
53+
54+
with = job["with"].is_a?(Hash) ? job["with"] : {}
55+
input_ref = with["control_plane_flow_ref"]
56+
uses_match = job["uses"].to_s.match(CONTROL_PLANE_FLOW_WORKFLOW)
57+
58+
unless uses_match
59+
abort "#{path}:#{job_name} has control_plane_flow_ref but no control-plane-flow reusable workflow" if input_ref
60+
61+
next
62+
end
63+
64+
uses_ref = uses_match[1]
65+
refs[uses_ref] << "#{path}:#{job_name}"
66+
67+
if input_ref
68+
refs[input_ref] << "#{path}:#{job_name}"
69+
abort "#{path}:#{job_name} mismatched cpflow refs: #{uses_ref}, #{input_ref}" if uses_ref != input_ref
70+
elsif job.key?("secrets")
71+
abort "#{path}:#{job_name} inherits secrets but is missing control_plane_flow_ref for #{uses_ref}"
72+
end
73+
end
74+
end
75+
76+
if refs.empty?
77+
puts "no upstream cpflow reusable workflow refs found"
78+
elsif refs.length > 1
79+
refs.each do |ref, paths|
80+
puts "#{ref}: #{paths.uniq.sort.join(', ')}"
81+
end
82+
abort "cpflow workflow wrappers use multiple upstream refs: #{refs.keys.sort.join(', ')}"
83+
else
84+
puts "cpflow refs: #{refs.keys.sort.join(', ')}"
85+
end
86+
RUBY
87+
4188
echo "==> actionlint"
4289
actionlint -ignore "SC2129" .github/workflows/cpflow-*.yml

0 commit comments

Comments
 (0)