Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,14 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Go for release PR automation
if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false'
if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true'
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
with:
go-version-file: cli/.go-version
cache-dependency-path: cli/**/go.sum

- name: Dispatch release PR checks
if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false'
if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true'
working-directory: cli
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
1 change: 0 additions & 1 deletion cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

### Bug Fixes

* Release PRs label the Unity package summary ([#1438](https://github.com/hatayama/unity-cli-loop/issues/1438)) ([085c13f](https://github.com/hatayama/unity-cli-loop/commit/085c13f619c3ec03c7a9be44ff56c436a398956e))
* Windows update checks no longer interrupt routine commands ([#1434](https://github.com/hatayama/unity-cli-loop/issues/1434)) ([02ce6cc](https://github.com/hatayama/unity-cli-loop/commit/02ce6cc3dbeb92360ebcd5c5318eae6b9ca87c7c))

## [3.0.0-beta.43](https://github.com/hatayama/unity-cli-loop/compare/cli-v3.0.0-beta.42...cli-v3.0.0-beta.43) (2026-06-28)
Expand Down
35 changes: 32 additions & 3 deletions cli/internal/automation/release_pr_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ const (

var (
releasePRCheckNow = time.Now
releasePRCheckSleep = time.Sleep
releasePRCheckSleep = func(ctx context.Context, duration time.Duration) error {
timer := time.NewTimer(duration)
defer timer.Stop()

select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}

releasePRCheckPlainUnityPackageSummary = regexp.MustCompile(`<details><summary>((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[A-Za-z0-9][A-Za-z0-9.-]*)?)</summary>`)
)
Expand Down Expand Up @@ -62,7 +72,7 @@ func RunReleasePleasePRChecks(ctx context.Context, stdout io.Writer, stderr io.W
return 1
}

releasePR, found, err := findReleasePRCheckPullRequest(ctx, config)
releasePR, found, err := findReleasePRCheckPullRequestWithRetry(ctx, config)
if err != nil {
writeReleasePRCheckLine(stderr, err)
return 1
Expand Down Expand Up @@ -220,6 +230,22 @@ func findReleasePRCheckPullRequest(ctx context.Context, config releasePRCheckCon
return matchingPRs[0], true, nil
}

func findReleasePRCheckPullRequestWithRetry(ctx context.Context, config releasePRCheckConfig) (releasePullRequest, bool, error) {
for attempt := 0; attempt < config.lookupAttempts; attempt++ {
releasePR, found, err := findReleasePRCheckPullRequest(ctx, config)
if err != nil || found {
return releasePR, found, err
}
if attempt+1 < config.lookupAttempts {
err = releasePRCheckSleep(ctx, time.Duration(config.lookupIntervalSeconds)*time.Second)
if err != nil {
return releasePullRequest{}, false, err
}
}
}
return releasePullRequest{}, false, nil
}

func releasePRCheckMatches(releasePR releasePullRequest, targetBranch string) bool {
releasePRBranch := "release-please--branches--" + targetBranch
if releasePR.HeadRefName != releasePRBranch && !strings.HasPrefix(releasePR.HeadRefName, releasePRBranch+"--components--") {
Expand Down Expand Up @@ -338,7 +364,10 @@ func findDispatchedReleasePRCheckRun(
return run, nil
}
if attempt+1 < config.lookupAttempts {
releasePRCheckSleep(time.Duration(config.lookupIntervalSeconds) * time.Second)
err = releasePRCheckSleep(ctx, time.Duration(config.lookupIntervalSeconds)*time.Second)
if err != nil {
return releaseWorkflowRun{}, err
}
}
}
return releaseWorkflowRun{}, fmt.Errorf("could not find dispatched %s workflow run for %s", config.workflow, releasePR.HeadRefOID)
Expand Down
65 changes: 57 additions & 8 deletions cli/internal/automation/release_pr_checks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,46 @@ func TestReleasePRChecksClarifyUnityPackageSummaryBeforeDispatch(t *testing.T) {
assertReleasePRCheckLogContains(t, result.ghLog, "workflow run build-and-test.yml --repo owner/repository --ref release-please--branches--v3-beta")
}

// Verifies that release PR lookup is retried while GitHub exposes the newly opened PR and label.
func TestReleasePRChecksRetryPendingReleasePRLookup(t *testing.T) {
result := runReleasePRCheckCase(t, releasePRCheckCase{
prListJSON: `[]`,
prListJSONAfterWatch: `[{"number":1043,"headRefName":"release-please--branches--v3-beta","headRefOid":"abc123","title":"chore: release v3-beta","url":"https://example.test/pr/1043"}]`,
runListJSON: `[{"databaseId":4242,"headSha":"abc123","createdAt":"2026-05-30T01:00:01Z","status":"queued","conclusion":"","url":"https://example.test/run/4242"}]`,
runWatchStatus: "0",
lookupAttempts: "2",
})

if result.exitCode != 0 {
t.Fatalf("expected exit code 0, got %d\nstderr: %s", result.exitCode, result.stderr)
}
if len(result.sleeps) != 1 || result.sleeps[0] != time.Second {
t.Fatalf("expected one initial lookup sleep, got %v", result.sleeps)
}
assertReleasePRCheckLogContains(t, result.stdout, "Marked release PR #1043 as draft while checks run.")
assertReleasePRCheckLogContainsLine(t, result.ghLog, "pr ready 1043 --repo owner/repository")
}

// Verifies that release PR lookup stops when the caller cancels the command.
func TestReleasePRChecksCancelPendingReleasePRLookup(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()

result := runReleasePRCheckCase(t, releasePRCheckCase{
ctx: ctx,
prListJSON: `[]`,
runListJSON: `[]`,
runWatchStatus: "0",
lookupAttempts: "2",
})

if result.exitCode != 1 {
t.Fatalf("expected exit code 1, got %d", result.exitCode)
}
assertReleasePRCheckLogContains(t, result.stderr, "context canceled")
assertReleasePRCheckLogDoesNotContain(t, result.ghLog, "workflow run")
}

// Verifies that same-second workflow runs are accepted when GitHub rounds createdAt timestamps.
func TestReleasePRChecksAcceptSameSecondRunAfterDispatch(t *testing.T) {
result := runReleasePRCheckCase(t, releasePRCheckCase{
Expand Down Expand Up @@ -216,11 +256,13 @@ func TestReleasePRChecksFailWhenHeadChangesBeforeReady(t *testing.T) {
}

type releasePRCheckCase struct {
ctx context.Context
prListJSON string
prListJSONAfterWatch string
prViewJSON string
runListJSON string
runWatchStatus string
lookupAttempts string
now time.Time
}

Expand Down Expand Up @@ -248,12 +290,16 @@ func runReleasePRCheckCase(t *testing.T, testCase releasePRCheckCase) releasePRC
if prListJSONAfterWatch == "" {
prListJSONAfterWatch = testCase.prListJSON
}
lookupAttempts := testCase.lookupAttempts
if lookupAttempts == "" {
lookupAttempts = "1"
}

t.Setenv("PATH", mockBin+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("GITHUB_REPOSITORY", "owner/repository")
t.Setenv("TARGET_BRANCH", "v3-beta")
t.Setenv("ULOOP_REPOSITORY_ROOT", workDir)
t.Setenv("RELEASE_PR_CHECK_LOOKUP_ATTEMPTS", "1")
t.Setenv("RELEASE_PR_CHECK_LOOKUP_ATTEMPTS", lookupAttempts)
t.Setenv("RELEASE_PR_CHECK_LOOKUP_INTERVAL_SECONDS", "1")
t.Setenv("RELEASE_PR_CHECK_WATCH_INTERVAL_SECONDS", "1")
t.Setenv("GH_PR_LIST_JSON", testCase.prListJSON)
Expand All @@ -275,28 +321,31 @@ func runReleasePRCheckCase(t *testing.T, testCase releasePRCheckCase) releasePRC
releasePRCheckNow = func() time.Time {
return now
}
releasePRCheckSleep = func(duration time.Duration) {
releasePRCheckSleep = func(ctx context.Context, duration time.Duration) error {
sleeps = append(sleeps, duration)
return ctx.Err()
}
t.Cleanup(func() {
releasePRCheckNow = originalNow
releasePRCheckSleep = originalSleep
})

ctx := testCase.ctx
if ctx == nil {
ctx = context.Background()
}

stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
exitCode := RunReleasePleasePRChecks(context.Background(), &stdout, &stderr)
ghLogBytes, err := os.ReadFile(ghLogPath)
if err != nil {
t.Fatalf("failed to read gh log: %v", err)
}
exitCode := RunReleasePleasePRChecks(ctx, &stdout, &stderr)
ghLog := readOptionalFile(t, ghLogPath)
gitLog := readOptionalFile(t, gitLogPath)

return releasePRCheckRunResult{
exitCode: exitCode,
stdout: stdout.String(),
stderr: stderr.String(),
ghLog: string(ghLogBytes),
ghLog: ghLog,
gitLog: gitLog,
sleeps: sleeps,
}
Expand Down
4 changes: 4 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"prerelease-type": "beta",
"include-v-in-tag": true,
"include-component-in-tag": true,
"exclude-paths": [
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"cli/cmd/dispatch-release-please-pr-checks",
"cli/internal/automation"
],
"changelog-path": "CHANGELOG.md",
"extra-files": [
{
Expand Down
6 changes: 4 additions & 2 deletions scripts/test-release-please-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ assert_json_value '.packages["."].["extra-files"][2].jsonpath' '$.packageVersion
assert_json_value '.packages["cli"].component' 'uloop-project-runner'
assert_json_value '.packages["cli"].["include-component-in-tag"]' 'true'
assert_json_value '.packages["cli"].["changelog-path"]' 'CHANGELOG.md'
assert_json_value '.packages["cli"].["exclude-paths"][0]' 'cli/cmd/dispatch-release-please-pr-checks'
assert_json_value '.packages["cli"].["exclude-paths"][1]' 'cli/internal/automation'
assert_json_value '.packages["cli"].["extra-files"] | length' '4'
assert_json_value '.packages["cli"].["extra-files"][0].path' 'internal/tools/default-tools.json'
assert_json_value '.packages["cli"].["extra-files"][1].path' 'contract.json'
Expand All @@ -153,8 +155,8 @@ assert_file_contains "$RELEASE_WORKFLOW" ' - name: Setup Go for release PR
assert_file_contains "$RELEASE_WORKFLOW" ' - name: Dispatch release PR checks'
assert_file_contains "$RELEASE_WORKFLOW" ' working-directory: cli'
assert_file_contains "$RELEASE_WORKFLOW" ' run: go run ./cmd/dispatch-release-please-pr-checks'
assert_step_contains "$RELEASE_WORKFLOW" ' - name: Setup Go for release PR automation' " if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false'"
assert_step_contains "$RELEASE_WORKFLOW" ' - name: Dispatch release PR checks' " if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false'"
assert_step_contains "$RELEASE_WORKFLOW" ' - name: Setup Go for release PR automation' " if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true'"
assert_step_contains "$RELEASE_WORKFLOW" ' - name: Dispatch release PR checks' " if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true'"
assert_file_order "$RELEASE_WORKFLOW" ' - name: Setup Go for package release sync' 'Sync release-please package releases'
assert_file_order "$RELEASE_WORKFLOW" ' - name: Setup Go for release PR automation' ' - name: Dispatch release PR checks'

Expand Down
Loading