Skip to content
Draft
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
380 changes: 303 additions & 77 deletions .github/scripts/monitor-remote-workflow.mjs

Large diffs are not rendered by default.

631 changes: 631 additions & 0 deletions .github/scripts/monitor-remote-workflow_test.mjs

Large diffs are not rendered by default.

50 changes: 24 additions & 26 deletions .github/workflows/__build-bicep-types.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
build-and-push-bicep-types:
name: Dispatch Bicep Types publish
runs-on: ubuntu-24.04
timeout-minutes: 15
timeout-minutes: 18
environment: publish-bicep
permissions:
contents: read # Required for actions/checkout
Expand All @@ -45,6 +45,15 @@ jobs:
- name: Parse release version and set environment variables
run: python ./.github/scripts/get_release_version.py

- name: Capture release metadata
id: release-metadata
shell: bash
run: |
{
echo "release_version=${REL_VERSION}"
echo "release_channel=${REL_CHANNEL}"
} >> "$GITHUB_OUTPUT"

- name: Get App Token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: get-token
Expand All @@ -58,29 +67,7 @@ jobs:
repositories: |
radius-publisher

- name: Capture dispatch start time
id: dispatch-start
shell: bash
run: |
echo "started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"

- name: Repository Dispatch
id: repository-dispatch
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ steps.get-token.outputs.token }}
repository: azure-octo/radius-publisher
event-type: bicep-types
client-payload: |-
{
"source_repository": "${{ github.repository }}",
"source_ref": "${{ github.ref }}",
"source_sha": "${{ github.sha }}",
"rel_channel": "${{ env.REL_CHANNEL }}",
"registry_target": "radius"
}

- name: Monitor remote workflow
- name: Dispatch or monitor correlated publisher run
id: monitor-remote-workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
Expand All @@ -92,8 +79,19 @@ jobs:
INPUT_OWNER: azure-octo
INPUT_REPO: radius-publisher
INPUT_WORKFLOW_FILE: publish-bicep-types.yml
INPUT_DISPATCH_STARTED_AT: ${{ steps.dispatch-start.outputs.started_at }}
INPUT_MAX_WAIT_SECONDS: "600"
INPUT_EVENT_TYPE: bicep-types
INPUT_RELEASE_IDENTIFIER: ${{ steps.release-metadata.outputs.release_version }}-${{ github.sha }}
# toJSON emits each value already quoted and escaped, so no ref or
# channel string can break out of the payload it is placed in.
INPUT_CLIENT_PAYLOAD: |-
{
"source_repository": ${{ toJSON(github.repository) }},
"source_ref": ${{ toJSON(github.ref) }},
"source_sha": ${{ toJSON(github.sha) }},
"rel_channel": ${{ toJSON(steps.release-metadata.outputs.release_channel) }},
"registry_target": "radius"
}
INPUT_MAX_WAIT_SECONDS: "720"
INPUT_POLL_INTERVAL_SECONDS: "15"

- name: Show failed logs
Expand Down
38 changes: 14 additions & 24 deletions .github/workflows/publish-de-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
dispatch-publish:
name: Dispatch DE image publish
runs-on: ubuntu-24.04
timeout-minutes: 15
timeout-minutes: 18
environment:
name: publish-de-image
permissions:
Expand Down Expand Up @@ -64,27 +64,7 @@ jobs:
repositories: |
radius-publisher

- name: Capture dispatch start time
id: dispatch-start
shell: bash
run: |
echo "started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"

- name: Repository Dispatch
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ steps.get-token.outputs.token }}
repository: azure-octo/radius-publisher
event-type: deployment-engine
client-payload: |-
{
"source_repository": "${{ github.repository }}",
"src_image": "${{ steps.payload.outputs.src_image }}",
"dest_image": "${{ steps.payload.outputs.dest_image }}",
"tag": "${{ steps.payload.outputs.tag }}"
}

- name: Monitor remote workflow
- name: Dispatch or monitor correlated publisher run
id: monitor-remote-workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
Expand All @@ -96,8 +76,18 @@ jobs:
INPUT_OWNER: azure-octo
INPUT_REPO: radius-publisher
INPUT_WORKFLOW_FILE: publish-deployment-engine.yml
INPUT_DISPATCH_STARTED_AT: ${{ steps.dispatch-start.outputs.started_at }}
INPUT_MAX_WAIT_SECONDS: "600"
INPUT_EVENT_TYPE: deployment-engine
INPUT_RELEASE_IDENTIFIER: ${{ steps.payload.outputs.tag }}-${{ github.run_id }}
# These values arrive from an external repository_dispatch, so emit
# them with toJSON rather than interpolating them into JSON text.
INPUT_CLIENT_PAYLOAD: |-
{
"source_repository": ${{ toJSON(github.repository) }},
"src_image": ${{ toJSON(steps.payload.outputs.src_image) }},
"dest_image": ${{ toJSON(steps.payload.outputs.dest_image) }},
"tag": ${{ toJSON(steps.payload.outputs.tag) }}
}
INPUT_MAX_WAIT_SECONDS: "720"
INPUT_POLL_INTERVAL_SECONDS: "15"

- name: Show failed logs
Expand Down
41 changes: 15 additions & 26 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ jobs:
# Must stay above the Deployment Engine monitor budget below
# (INPUT_MAX_WAIT_SECONDS) plus checkout and reconciliation time, otherwise
# the job is killed while the monitor is still waiting.
timeout-minutes: 20
timeout-minutes: 25
environment: release
permissions:
contents: read # Required for actions/checkout
Expand Down Expand Up @@ -332,29 +332,7 @@ jobs:
repositories: |
radius-publisher

- name: Capture DE dispatch start time
if: success() && steps.release-should-skip.outputs.result == 'false'
id: de-dispatch-start
shell: bash
run: |
echo "started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"

- name: Dispatch Deployment Engine image publish
if: success() && steps.release-should-skip.outputs.result == 'false'
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ steps.get-de-token.outputs.token }}
repository: azure-octo/radius-publisher
event-type: deployment-engine
client-payload: |-
{
"source_repository": "${{ github.repository }}",
"src_image": "radiusdeploymentengine.azurecr.io/deployment-engine",
"dest_image": "ghcr.io/radius-project/deployment-engine",
"tag": "${{ steps.get-version.outputs.release-channel }}"
}

- name: Monitor DE image publish workflow
- name: Dispatch or monitor correlated DE publisher run
if: success() && steps.release-should-skip.outputs.result == 'false'
id: monitor-de-workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
Expand All @@ -367,8 +345,19 @@ jobs:
INPUT_OWNER: azure-octo
INPUT_REPO: radius-publisher
INPUT_WORKFLOW_FILE: publish-deployment-engine.yml
INPUT_DISPATCH_STARTED_AT: ${{ steps.de-dispatch-start.outputs.started_at }}
INPUT_MAX_WAIT_SECONDS: "600"
INPUT_EVENT_TYPE: deployment-engine
INPUT_RELEASE_IDENTIFIER: ${{ steps.get-version.outputs.release-version }}-${{ github.sha }}
# toJSON emits each value already quoted and escaped, so no computed
# version string can break out of the payload it is placed in.
INPUT_CLIENT_PAYLOAD: |-
{
"source_repository": ${{ toJSON(github.repository) }},
"source_sha": ${{ toJSON(github.sha) }},
"src_image": "radiusdeploymentengine.azurecr.io/deployment-engine",
"dest_image": "ghcr.io/radius-project/deployment-engine",
"tag": ${{ toJSON(steps.get-version.outputs.release-channel) }}
}
INPUT_MAX_WAIT_SECONDS: "720"
INPUT_POLL_INTERVAL_SECONDS: "15"

- name: Show failed DE publish logs
Expand Down
6 changes: 5 additions & 1 deletion build/test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ GOTEST_OPTS ?=
GOTEST_TOOL ?= go tool gotestsum $(GOTESTSUM_OPTS) --

.PHONY: test
test: test-get-envtools test-helm test-manage-radius-installation test-release-parity-manifest test-verify-goreleaser-snapshot test-changelog-range test-changelog-config test-build-summary test-goreleaser-shadow test-capture-release-image-digests test-release-get-version test-release-tag-and-branch ## Runs unit tests, excluding kubernetes controller tests
test: test-get-envtools test-helm test-manage-radius-installation test-release-parity-manifest test-verify-goreleaser-snapshot test-changelog-range test-changelog-config test-build-summary test-goreleaser-shadow test-capture-release-image-digests test-release-get-version test-release-tag-and-branch test-monitor-remote-workflow ## Runs unit tests, excluding kubernetes controller tests
KUBEBUILDER_ASSETS="$(shell $(ENV_SETUP) use -p path ${K8S_VERSION} --arch amd64)" CGO_ENABLED=1 $(GOTEST_TOOL) ./pkg/... ./test/validation/... $(GOTEST_OPTS)

.PHONY: test-manage-radius-installation
Expand Down Expand Up @@ -96,6 +96,10 @@ test-release-tag-and-branch: ## Tests release tag and branch reconciliation
test-release-get-version: ## Tests release version selection across repositories
@bash ./.github/scripts/release-get-version_test.sh

.PHONY: test-monitor-remote-workflow
test-monitor-remote-workflow: ## Tests exact remote workflow dispatch correlation
@node --test ./.github/scripts/monitor-remote-workflow_test.mjs

.PHONY: test-compile
test-compile: test-get-envtools ## Compiles all tests without running them
@echo "$(ARROW) Compiling unit tests..."
Expand Down
23 changes: 12 additions & 11 deletions eng/design-notes/tools/2026-09-goreleaser-stack-review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ This directory records the review of the 18-pull-request stack that implements t

## Review notes by pull request

| PR | Branch | Note |
|----|-----------------------------------------|----------------------------------------------------------------------------|
| 1 | `dp/goreleaser-parity-manifest` | [pr-01-parity-manifest.md](./pr-01-parity-manifest.md) |
| 2 | `dp/goreleaser-snapshot-ci` | [pr-02-goreleaser-snapshot.md](./pr-02-goreleaser-snapshot.md) |
| 3 | `dp/conventional-commit-title-check` | [pr-03-conventional-commit-title.md](./pr-03-conventional-commit-title.md) |
| 4 | `dp/git-cliff-changelog-bootstrap` | [pr-04-git-cliff-changelog.md](./pr-04-git-cliff-changelog.md) |
| 5 | `dp/split-build-workflows` | [pr-05-split-build-workflows.md](./pr-05-split-build-workflows.md) |
| 6 | `dp/edge-tags-deprecation` | [pr-06-edge-tags.md](./pr-06-edge-tags.md) |
| 7 | `dp/goreleaser-shadow-release` | [pr-07-goreleaser-shadow.md](./pr-07-goreleaser-shadow.md) |
| 8 | `dp/conventional-commit-title-required` | [pr-08-title-check-required.md](./pr-08-title-check-required.md) |
| 9 | `dp/idempotent-tag-reconciliation` | [pr-09-tag-reconciliation.md](./pr-09-tag-reconciliation.md) |
| PR | Branch | Note |
|----|-----------------------------------------|--------------------------------------------------------------------------------------|
| 1 | `dp/goreleaser-parity-manifest` | [pr-01-parity-manifest.md](./pr-01-parity-manifest.md) |
| 2 | `dp/goreleaser-snapshot-ci` | [pr-02-goreleaser-snapshot.md](./pr-02-goreleaser-snapshot.md) |
| 3 | `dp/conventional-commit-title-check` | [pr-03-conventional-commit-title.md](./pr-03-conventional-commit-title.md) |
| 4 | `dp/git-cliff-changelog-bootstrap` | [pr-04-git-cliff-changelog.md](./pr-04-git-cliff-changelog.md) |
| 5 | `dp/split-build-workflows` | [pr-05-split-build-workflows.md](./pr-05-split-build-workflows.md) |
| 6 | `dp/edge-tags-deprecation` | [pr-06-edge-tags.md](./pr-06-edge-tags.md) |
| 7 | `dp/goreleaser-shadow-release` | [pr-07-goreleaser-shadow.md](./pr-07-goreleaser-shadow.md) |
| 8 | `dp/conventional-commit-title-required` | [pr-08-title-check-required.md](./pr-08-title-check-required.md) |
| 9 | `dp/idempotent-tag-reconciliation` | [pr-09-tag-reconciliation.md](./pr-09-tag-reconciliation.md) |
| 10 | `dp/release-identifier-correlation` | [pr-10-release-identifier-correlation.md](./pr-10-release-identifier-correlation.md) |

Later notes are added as the review progresses up the stack.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Review note: PR 10 - Release-identifier correlation for remote dispatch

- **Pull request**: [#12783](https://github.com/radius-project/radius/pull/12783)
- **Plan phase**: [PR 10](../2026-03-goreleaser-release-lifecycle-implementation-plan.md#pr-10-release-identifier-correlation-for-remote-dispatch)
- **Stack index**: [README](./README.md)

## Verdict

The layer does what the plan asks and implements the design's idempotency rule for remote publishers. Each caller hands the publisher a stable identifier, and the monitor looks for the exact run name before it dispatches anything: an existing successful run is reused, an active run is monitored to completion, a failed run is retried by a new dispatch under the same identifier, and only an absent run leads to a dispatch. The companion publisher change ([azure-octo/radius-publisher#25](https://github.com/azure-octo/radius-publisher/pull/25)) renders `run-name` as `<event type> / <identifier>` and keys its concurrency groups by the same identifier without cancellation, which is exactly the string and the behavior the monitor expects; dispatches without an identifier fall back to the publisher run id, so they can never collide with a release. The identifier choices fit each caller: version plus source commit for the release and Bicep dispatches, so a rerun at the same commit reuses the publication, and tag plus run id for the Deployment Engine bridge, whose dispatches must publish every time.

Discovery walks the publisher's whole `repository_dispatch` history under the monitor's deadline instead of a page cap, which is the right trade: the earlier five-page bound assumed that one identifier's runs are contiguous, and a retry days later is not. Today that costs seven requests for the Bicep publisher (607 retained runs) and two for the Deployment Engine publisher (186), well inside the budget of an App token. Payload values are emitted with `toJSON`, so an externally supplied image name, tag, or ref cannot break out of the JSON it is placed in. Transient API failures are retried with randomized exponential backoff inside one total budget, an uncertain dispatch is reconciled before any retry, and the job timeouts (18 and 25 minutes) sit above the 12-minute monitor budget. The hermetic scenarios cover reuse, retry, races, pagination, timeouts, and the caller wiring; Prettier and actionlint are clean.

## Changes made in this review

### 1. Failures name the call that failed

- **What changed**: an error that ends the monitor now says which operation failed and after how many attempts. A dispatch that exhausts its retries also says that no run with the expected name appeared and that a rerun reconciles before dispatching again, and the lookup-timeout message says that the monitor budget ran out before every history page was read. Two scenarios pin the exhausted-lookup and exhausted-dispatch messages, and the non-retryable case asserts the full text.
- **Why**: the raw GitHub error ("Not Found", "Bad Gateway") was the whole job annotation. It did not say whether the run lookup, the dispatch, or a status poll had failed, nor whether the dispatch may have been accepted, while the design asks every error summary to name the stage, the observed state, and the recovery action.
- **Value**: a release engineer reads the annotation and knows what to check and that rerunning the job is safe.
- **Impact**: messages only, no control-flow change; 19 scenarios pass.

## Findings left as-is

- **Retry by dispatch**: the design's idempotency contract says the controller "reruns a failed retryable run"; this layer dispatches a new run under the same identifier instead of calling the rerun API. The outcome is the same, it needs no `actions: write` on the publisher, which the App token does not hold, and a rerun would pin the publisher's old workflow definition. The design text needs no change.
- **Full-history discovery**: bounded by the monitor deadline, and measured above at seven and two requests per dispatch. A `created` window would cut this but would make a late resume miss its run; not worth it at this size.
- **`Capture release metadata` step in `__build-bicep-types.yaml`**: the values it copies are also available through the `env` context, so the step could go, but PR 18 edits the surrounding lines and the step keeps the identifier expression explicit in one place.
- **Node version in the unit-test job**: `make test` runs `node --test` with the runner image's Node 22 rather than the Node 24 pinned in `.node-version`; `node:test` behaves the same on both, and the lint job pins the version for Prettier.
- **Companion ordering**: the publisher pull request must be deployed before this layer merges, as the pull request body says; the publisher's current run titles are still the plain event names.

## Verification

- 19 scenarios pass; Prettier with the repository configuration passes for the script, the test, and the three workflows; actionlint passes for the three workflows.
- The companion pull request's head was read for `run-name`, the dispatch `types`, and `concurrency`; the history sizes come from the workflow-runs API of the publisher repository.
Loading