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/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ ci:
- e2e/**
- go.mod
- go.sum
cli_version: v0.1.0
cli_version_sha: 9dc69a1f66753a3865c38c34eca5a931f677c803
cli_version: v0.8.0
cli_version_sha: 58c3d309faaf5028aa03900aded8424e738afb81
pin_mode: sha
manifest_file: .github/manifest.yaml
manifest_key: ci
Expand Down
29 changes: 26 additions & 3 deletions .github/workflows/nightly-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,13 @@ jobs:
# rehearsal is always among the newest) and deletes the older accumulated
# ones, so they cannot pile up and pollute version-calc.
#
# Always on the schedule; on a manual run only when this is not itself a
# dry run, so it never races the tag the dispatch job just cut.
if: github.event_name == 'schedule' || github.event.inputs.dry_run != 'true'
# Always runs, including on a dry-run dispatch: retention only ever deletes
# tags OLDER than the newest few, so the tag a concurrent dispatch job is
# cutting (always the newest by monotonic run number) can never be among
# them even if this job races ahead of that push. Skipping the whole job on
# a dry-run dispatch (the previous behavior) left pruning off for as long as
# a maintainer kept rehearsing back to back, which is exactly when dry-run
# tags accumulate fastest.
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -293,6 +297,25 @@ jobs:

git fetch --tags --force --prune --prune-tags --quiet origin

# A dry-run release can outlive its tag: if the tag is ever removed
# out-of-band (e.g. a manual `git push --delete` that does not also
# delete the release object), the release is left dangling with no
# ref to find it by. A tag-only scan can never see it again, so it
# accumulates invisibly forever. Reconcile those first: with no live
# tag there is no in-flight rehearsal chain to protect, so retention
# does not apply and each one is deleted unconditionally.
mapfile -t releases < <(
gh release list -L 200 --json tagName --jq '.[].tagName' \
| grep -E "$dryrun_re" || true
)
for tag in "${releases[@]}"; do
if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null 2>&1; then
continue # has a live tag; handled by the retention pass below
fi
gh release delete "$tag" --yes
echo "::notice::Deleted orphaned dry-run release ${tag} (tag already gone)."
done

mapfile -t tags < <(git tag -l | grep -E "$dryrun_re" | sort -V || true)

count=${#tags[@]}
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/orchestrate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ jobs:
with:
fetch-depth: 0
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@9dc69a1f66753a3865c38c34eca5a931f677c803 # v0.1.0
uses: stablekernel/cascade/.github/actions/setup-cli@58c3d309faaf5028aa03900aded8424e738afb81 # v0.8.0
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.1.0
version: v0.8.0
- name: Run Setup
id: setup
run: |
Expand Down Expand Up @@ -109,10 +109,10 @@ jobs:
echo "_No outputs produced_" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@9dc69a1f66753a3865c38c34eca5a931f677c803 # v0.1.0
uses: stablekernel/cascade/.github/actions/setup-cli@58c3d309faaf5028aa03900aded8424e738afb81 # v0.8.0
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.1.0
version: v0.8.0
- name: Generate Changelog
id: changelog
env:
Expand Down Expand Up @@ -149,7 +149,7 @@ jobs:
# The tag-push trigger is unreliable here: the candidate tag can
# point at a state commit whose message suppresses CI, so an explicit
# dispatch against the tag is the dependable way to build the candidate.
gh workflow run ./.github/workflows/release.yaml \
gh workflow run release.yaml \
--repo "${{ github.repository }}" \
--ref "$TAG"
- name: Update Manifest
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/promote.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ jobs:
with:
fetch-depth: 0
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@9dc69a1f66753a3865c38c34eca5a931f677c803 # v0.1.0
uses: stablekernel/cascade/.github/actions/setup-cli@58c3d309faaf5028aa03900aded8424e738afb81 # v0.8.0
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.1.0
version: v0.8.0
- name: Run Preflight
id: preflight
env:
Expand Down Expand Up @@ -128,10 +128,10 @@ jobs:
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@9dc69a1f66753a3865c38c34eca5a931f677c803 # v0.1.0
uses: stablekernel/cascade/.github/actions/setup-cli@58c3d309faaf5028aa03900aded8424e738afb81 # v0.8.0
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.1.0
version: v0.8.0
- name: Validate Promotion
env:
MODE: ${{ github.event.inputs.mode }}
Expand All @@ -155,10 +155,10 @@ jobs:
with:
fetch-depth: 0
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@9dc69a1f66753a3865c38c34eca5a931f677c803 # v0.1.0
uses: stablekernel/cascade/.github/actions/setup-cli@58c3d309faaf5028aa03900aded8424e738afb81 # v0.8.0
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.1.0
version: v0.8.0
- name: Generate Changelog
id: changelog
env:
Expand Down Expand Up @@ -273,7 +273,7 @@ jobs:
run: |
# Dispatch the configured release-build workflow against the
# published tag so it can build and attach release binaries.
gh workflow run ./.github/workflows/release.yaml \
gh workflow run release.yaml \
--repo "${{ github.repository }}" \
--ref "$TAG"

Expand Down
2 changes: 1 addition & 1 deletion e2e/scenarios/38-promote-breaking-gate-release-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ steps:
- " if: ${{ github.event.inputs.dry_run != 'true' }}\n"
# Follow-on release-build dispatch against the published tag.
- " - name: Trigger Release Build\n"
- " gh workflow run ./.github/workflows/release-build.yaml \\\n"
- " gh workflow run release-build.yaml \\\n"
not_contains:
# The promote path uses the CLI gate, not the release bash step.
- " - name: Check Breaking Changes\n"
13 changes: 8 additions & 5 deletions internal/generate/candidate_dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ func TestGenerator_CandidateDispatchStep(t *testing.T) {
require.Contains(t, content, "- name: Dispatch Release Candidate Build",
"expected the candidate dispatch step in dispatch mode with a release workflow")
body := stepRunBody(t, content, "Dispatch Release Candidate Build")
assert.Contains(t, body, "gh workflow run ./.github/workflows/release.yaml",
"the step must dispatch the configured release workflow, normalized")
assert.Contains(t, body, "gh workflow run release.yaml",
"the step must dispatch the configured release workflow by its bare file name; "+
"gh workflow run 404s on a \"./\"-prefixed repo path")
assert.Contains(t, body, `--ref "$TAG"`,
"the dispatch must target the candidate tag")
} else {
Expand All @@ -89,14 +90,16 @@ func TestGenerator_CandidateDispatchStep(t *testing.T) {
}

// TestGenerator_CandidateDispatchNormalizesBareFilename asserts a bare release
// workflow filename is normalized to a repo-relative path in the dispatch.
// workflow filename dispatches unchanged: gh workflow run addresses a same-repo
// workflow by file name (or numeric ID), never by a "./"-prefixed repository
// path, so an already-bare name needs no further normalization.
func TestGenerator_CandidateDispatchNormalizesBareFilename(t *testing.T) {
cfg, tmpDir := candidateDispatchConfig(t, true, &config.ReleaseConfig{Workflow: "release.yaml"})

content, err := NewGenerator(cfg, tmpDir).Generate()
require.NoError(t, err)

body := stepRunBody(t, content, "Dispatch Release Candidate Build")
assert.Contains(t, body, "gh workflow run ./.github/workflows/release.yaml",
"a bare release workflow filename must normalize to a repo-relative path")
assert.Contains(t, body, "gh workflow run release.yaml",
"a bare release workflow filename must dispatch unchanged")
}
20 changes: 19 additions & 1 deletion internal/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ func normalizeWorkflowPath(path string) string {
return "./.github/workflows/" + filepath.Base(path)
}

// workflowDispatchTarget returns the value cascade passes as the positional
// target of "gh workflow run" to dispatch a same-repo workflow. Every call site
// in this package always passes --repo explicitly and targets the current
// repository, so gh needs only the bare workflow file name (or a numeric
// workflow ID); the "./"-prefixed repository path normalizeWorkflowPath
// produces is correct for a uses: reusable-workflow reference but 404s against
// gh workflow run ("workflow ... not found on the default branch"). A leading
// "@ref" cross-repo suffix (valid for uses:, meaningless for gh workflow run,
// whose target repo comes from --repo) is stripped defensively so a
// misconfigured external-style value still degrades to a usable file name
// rather than a broken dispatch argument.
func workflowDispatchTarget(path string) string {
if idx := strings.Index(path, "@"); idx != -1 {
path = path[:idx]
}
return filepath.Base(path)
}

// envGHAName returns the GitHub Environment name for a given cascade environment
// name. When the config has an EnvironmentConfig entry for that env whose
// GHAEnvironment field is non-empty, that value is returned; otherwise the
Expand Down Expand Up @@ -1925,7 +1943,7 @@ func (g *Generator) writeCandidateDispatchStep(sb *strings.Builder) {
sb.WriteString(" # The tag-push trigger is unreliable here: the candidate tag can\n")
sb.WriteString(" # point at a state commit whose message suppresses CI, so an explicit\n")
sb.WriteString(" # dispatch against the tag is the dependable way to build the candidate.\n")
sb.WriteString(" gh workflow run " + normalizeWorkflowPath(g.config.Release.Workflow) + " \\\n")
sb.WriteString(" gh workflow run " + workflowDispatchTarget(g.config.Release.Workflow) + " \\\n")
sb.WriteString(" --repo \"${{ github.repository }}\" \\\n")
sb.WriteString(" --ref \"$TAG\"\n")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/generate/promote.go
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ func (g *PromoteGenerator) writeFinalizeJob(sb *strings.Builder) {
sb.WriteString(" run: |\n")
sb.WriteString(" # Dispatch the configured release-build workflow against the\n")
sb.WriteString(" # published tag so it can build and attach release binaries.\n")
sb.WriteString(" gh workflow run " + normalizeWorkflowPath(g.config.Release.Workflow) + " \\\n")
sb.WriteString(" gh workflow run " + workflowDispatchTarget(g.config.Release.Workflow) + " \\\n")
sb.WriteString(" --repo \"${{ github.repository }}\" \\\n")
sb.WriteString(" --ref \"$TAG\"\n\n")
}
Expand Down Expand Up @@ -1264,7 +1264,7 @@ func (g *PromoteGenerator) writeFinalizeJob(sb *strings.Builder) {
bName := b.Name
sb.WriteString(" ARTIFACT_ID_" + strings.ToUpper(strings.ReplaceAll(bName, "-", "_")))
sb.WriteString("=$(yq eval \".$MANIFEST_KEY.state.$SOURCE_ENV.builds." + bName + ".artifact_id // \\\"\\\"\" \"$MANIFEST_FILE\")\n")
sb.WriteString(" gh workflow run " + normalizeWorkflowPath(g.config.Publish.Workflow) + " \\\n")
sb.WriteString(" gh workflow run " + workflowDispatchTarget(g.config.Publish.Workflow) + " \\\n")
sb.WriteString(" --repo \"${{ github.repository }}\" \\\n")
sb.WriteString(" --ref \"$NEW_VERSION\" \\\n")
sb.WriteString(" -f build_name=" + bName + " \\\n")
Expand Down
6 changes: 3 additions & 3 deletions internal/generate/release_build_dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ func TestPromoteGenerator_ReleaseBuildDispatch(t *testing.T) {
name: "release workflow configured emits a dispatch to it",
release: &config.ReleaseConfig{Workflow: ".github/workflows/release-build.yaml"},
wantStep: true,
wantDispatch: "gh workflow run ./.github/workflows/release-build.yaml",
wantDispatch: "gh workflow run release-build.yaml",
},
{
name: "bare release workflow filename is normalized",
name: "bare release workflow filename dispatches unchanged",
release: &config.ReleaseConfig{Workflow: "release-build.yaml"},
wantStep: true,
wantDispatch: "gh workflow run ./.github/workflows/release-build.yaml",
wantDispatch: "gh workflow run release-build.yaml",
},
}

Expand Down