Repository navigation
Share realm sync deployment tracking through a composite action #6920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
habdelra
merged 2 commits into
main
from
cs-14258-realm-sync-workflows-copy-the-same-deployment-tracking-shell
Oct 22, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| name: Finish a realm deployment | ||
| description: | | ||
| Marks a deployment realm-deployment/start recorded as succeeded or failed, | ||
| from the outcome of the step that pushed. Run it with | ||
| `if: always() && steps.<start>.outputs.deployment-id`, so a push that failed | ||
| or never ran still marks it failed. The next sync then compares against its | ||
| revision too, since the realm may hold part of it. | ||
|
|
||
| inputs: | ||
| deployment-id: | ||
| description: The `deployment-id` output of realm-deployment/start. | ||
| required: true | ||
| outcome: | ||
| description: The push step's outcome. Anything but `success` marks the deployment failed. | ||
| required: true | ||
| realm-url: | ||
| description: The realm the push wrote to, recorded as the environment URL. | ||
| required: true | ||
| token: | ||
| description: "A token with `deployments: write` on the repository." | ||
| default: ${{ github.token }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Finish the deployment record | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| env: | ||
| DEPLOYMENT_ID: ${{ inputs.deployment-id }} | ||
| OUTCOME: ${{ inputs.outcome }} | ||
| REALM_URL: ${{ inputs.realm-url }} | ||
| with: | ||
| github-token: ${{ inputs.token }} | ||
| script: | | ||
| await github.rest.repos.createDeploymentStatus({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| deployment_id: Number(process.env.DEPLOYMENT_ID), | ||
| state: process.env.OUTCOME === 'success' ? 'success' : 'failure', | ||
| environment_url: process.env.REALM_URL, | ||
| log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| #!/usr/bin/env bash | ||
| # Finds the revisions a realm may hold, and the `boxel realm push` arguments | ||
| # that skip a file only when it's unchanged against every one of them. The | ||
| # start action runs it; its action.yml describes the inputs, which arrive here | ||
| # as environment variables. Writes `revisions` and `push-args` to | ||
| # $GITHUB_OUTPUT. | ||
| # | ||
| # The newest deployment to the environment that succeeded names a revision the | ||
| # realm holds in full. Each deployment recorded after it failed or never | ||
| # finished, so the realm may hold part of its revision too. A file any of them | ||
| # may have written is therefore sent again. KNOWN_REVISION, when set, is a | ||
| # revision the realm is known to hold, and stands in for a successful | ||
| # deployment when none is found. | ||
| # | ||
| # With no revision to compare against, or one the checkout's history doesn't | ||
| # reach, the push sends every file, unless REQUIRE_REVISIONS refuses that. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| : "${ENVIRONMENT:?}" "${GITHUB_REPOSITORY:?}" "${GITHUB_OUTPUT:?}" | ||
| REPOSITORY_PATH="${REPOSITORY_PATH:-.}" | ||
| KNOWN_REVISION="${KNOWN_REVISION:-}" | ||
| REQUIRE_REVISIONS="${REQUIRE_REVISIONS:-false}" | ||
| title="$ENVIRONMENT realm sync" | ||
|
|
||
| # The push arguments come from the installed boxel-cli's --help, so a missing | ||
| # CLI would read as an old one and the push would send every file. | ||
| command -v boxel > /dev/null || { | ||
| echo "::error title=$title::boxel isn't on PATH. Run realm-deployment/start after installing @cardstack/boxel-cli." | ||
| exit 1 | ||
| } | ||
|
|
||
| revisions="" | ||
| add_revision() { | ||
| case " $revisions " in | ||
| *" $1 "*) ;; | ||
| *) revisions="${revisions:+$revisions }$1" ;; | ||
| esac | ||
| } | ||
|
|
||
| if [ -n "$KNOWN_REVISION" ]; then | ||
| add_revision "$KNOWN_REVISION" | ||
| fi | ||
|
|
||
| # A failed listing reads as no deployments, the same as a repository that has | ||
| # never recorded one. | ||
| rows=$(gh api "repos/$GITHUB_REPOSITORY/deployments?environment=$ENVIRONMENT&per_page=30" --jq '.[] | "\(.id):\(.sha)"') || { | ||
| echo "::warning title=$title::Couldn't list the $ENVIRONMENT deployments." | ||
| rows="" | ||
| } | ||
| succeeded=false | ||
| for row in $rows; do | ||
| add_revision "${row#*:}" | ||
| if [ "$(gh api "repos/$GITHUB_REPOSITORY/deployments/${row%%:*}/statuses?per_page=100" --jq 'any(.[]; .state == "success")' || true)" = "true" ]; then | ||
| succeeded=true | ||
| break | ||
| fi | ||
| done | ||
|
|
||
| if [ "$succeeded" != "true" ] && [ -z "$KNOWN_REVISION" ]; then | ||
| echo "::notice title=$title::No successful $ENVIRONMENT deployment is recorded, so this sync sends every file." | ||
| revisions="" | ||
| fi | ||
| for revision in $revisions; do | ||
| if ! git -C "$REPOSITORY_PATH" cat-file -e "$revision^{commit}" 2>/dev/null; then | ||
| echo "::warning title=$title::$revision, which the $ENVIRONMENT realm may hold, isn't in this checkout's history, so this sync sends every file." | ||
| revisions="" | ||
| break | ||
| fi | ||
| done | ||
| echo "The $ENVIRONMENT realm may hold: ${revisions:-(unknown)}" | ||
|
|
||
| # A boxel-cli that doesn't list an option rejects it, so each one is passed | ||
| # only when `realm push --help` lists it. | ||
| push_help=$(boxel realm push --help 2>&1 || true) | ||
| push_args=() | ||
| if grep -q -- '--changed-since' <<< "$push_help"; then | ||
| for revision in $revisions; do | ||
| push_args+=(--changed-since "$revision") | ||
| done | ||
| elif [ -n "$revisions" ]; then | ||
| echo "::notice title=$title::This boxel-cli has no --changed-since, so every file goes up." | ||
| fi | ||
|
|
||
| if [ "$REQUIRE_REVISIONS" = "true" ] && [ ${#push_args[@]} -eq 0 ]; then | ||
| echo "::error title=$title::No revision to compare against, so the push would send every file." | ||
| exit 1 | ||
| fi | ||
|
|
||
| # A sync is system-initiated, so the realm indexes it without announcing it to | ||
| # its open tabs, which pick the new content up on their next load instead of | ||
| # resetting mid-session. | ||
| if grep -q -- '--no-realm-events' <<< "$push_help"; then | ||
| push_args+=(--no-realm-events) | ||
| echo "Pushing with --no-realm-events: open tabs pick the changes up on their next load." | ||
| else | ||
| echo "::notice title=$title::This boxel-cli has no --no-realm-events, so the push is announced to open tabs." | ||
| fi | ||
|
|
||
| { | ||
| echo "revisions=$revisions" | ||
| echo "push-args=${push_args[*]}" | ||
| } >> "$GITHUB_OUTPUT" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| #!/usr/bin/env bash | ||
| # Runs start.sh against a scratch git repository, with `gh` and `boxel` | ||
| # replaced by stubs, and checks the revisions and push arguments it writes. | ||
| # | ||
| # The `gh` stub serves the deployment listing from $STUB_DIR/deployments, one | ||
| # `id:sha` per line, newest first, and reports a deployment as succeeded when | ||
| # its id is in $STUB_DIR/succeeded. The `boxel` stub's `realm push --help` | ||
| # prints $STUB_DIR/push-help. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| work="$(mktemp -d)" | ||
| trap 'rm -rf "$work"' EXIT | ||
|
|
||
| export STUB_DIR="$work/stub" | ||
| mkdir -p "$STUB_DIR/bin" | ||
| cat > "$STUB_DIR/bin/gh" <<'EOF' | ||
| #!/usr/bin/env bash | ||
| case "$2" in | ||
| */deployments\?*) | ||
| [ -f "$STUB_DIR/list-fails" ] && exit 1 | ||
| cat "$STUB_DIR/deployments" ;; | ||
| */deployments/*/statuses*) | ||
| id="${2#*/deployments/}" | ||
| id="${id%%/*}" | ||
| if grep -qx "$id" "$STUB_DIR/succeeded"; then echo true; else echo false; fi ;; | ||
| *) echo "unexpected gh call: $*" >&2; exit 2 ;; | ||
| esac | ||
| EOF | ||
| cat > "$STUB_DIR/bin/boxel" <<'EOF' | ||
| #!/usr/bin/env bash | ||
| cat "$STUB_DIR/push-help" | ||
| EOF | ||
| chmod +x "$STUB_DIR/bin/gh" "$STUB_DIR/bin/boxel" | ||
| export PATH="$STUB_DIR/bin:$PATH" | ||
|
|
||
| repo="$work/repo" | ||
| git init -q "$repo" | ||
| commit() { | ||
| git -C "$repo" -c user.name=test -c user.email=test@example.com commit -q --allow-empty -m "$1" | ||
| git -C "$repo" rev-parse HEAD | ||
| } | ||
| a=$(commit a) | ||
| b=$(commit b) | ||
| c=$(commit c) | ||
| outside=0123456789abcdef0123456789abcdef01234567 | ||
|
|
||
| export GITHUB_REPOSITORY=example/repo | ||
| export ENVIRONMENT=staging | ||
| export REPOSITORY_PATH="$repo" | ||
| both_flags=$' --changed-since <revision>\n --no-realm-events' | ||
|
|
||
| failures=0 | ||
| # run <name> <expected exit> <expected revisions> <expected push-args>, after | ||
| # arranging the stubs with `deployments`, `succeeded` and `help`. Extra | ||
| # environment for start.sh goes before the call. | ||
| run() { | ||
| local name="$1" want_status="$2" want_revisions="$3" want_args="$4" | ||
| export GITHUB_OUTPUT="$work/output" | ||
| : > "$GITHUB_OUTPUT" | ||
| local status=0 | ||
| bash "$here/start.sh" > "$work/log" 2>&1 || status=$? | ||
| local revisions push_args | ||
| revisions=$(sed -n 's/^revisions=//p' "$GITHUB_OUTPUT") | ||
| push_args=$(sed -n 's/^push-args=//p' "$GITHUB_OUTPUT") | ||
| if [ "$status" != "$want_status" ] || [ "$revisions" != "$want_revisions" ] || [ "$push_args" != "$want_args" ]; then | ||
| echo "FAIL: $name" | ||
| echo " exit: $status (want $want_status)" | ||
| echo " revisions: '$revisions' (want '$want_revisions')" | ||
| echo " push-args: '$push_args' (want '$want_args')" | ||
| sed 's/^/ | /' "$work/log" | ||
| failures=$((failures + 1)) | ||
| else | ||
| echo "ok: $name" | ||
| fi | ||
| } | ||
| deployments() { printf '%s\n' "$@" > "$STUB_DIR/deployments"; } | ||
| succeeded() { printf '%s\n' "$@" > "$STUB_DIR/succeeded"; } | ||
| cli_help() { printf '%s\n' "$1" > "$STUB_DIR/push-help"; } | ||
|
|
||
| deployments 3:"$c" 2:"$b" 1:"$a" | ||
| succeeded 2 | ||
| cli_help "$both_flags" | ||
| run "compares against the newest success and every deployment after it" 0 \ | ||
| "$c $b" "--changed-since $c --changed-since $b --no-realm-events" | ||
|
|
||
| deployments 4:"$c" 3:"$b" 2:"$c" 1:"$a" | ||
| succeeded 1 | ||
| run "lists a revision deployed twice once" 0 \ | ||
| "$c $b $a" "--changed-since $c --changed-since $b --changed-since $a --no-realm-events" | ||
|
|
||
| deployments 2:"$c" 1:"$b" | ||
| succeeded | ||
| run "sends every file with no successful deployment" 0 "" "--no-realm-events" | ||
| grep -q 'No successful staging deployment' "$work/log" || { echo "FAIL: no notice for a missing success"; failures=$((failures + 1)); } | ||
|
|
||
| deployments 2:"$outside" 1:"$b" | ||
| succeeded 1 | ||
| run "sends every file when a revision is outside the history" 0 "" "--no-realm-events" | ||
| grep -q "$outside, which the staging realm may hold, isn't in this checkout's history" "$work/log" || { echo "FAIL: no warning for a revision outside the history"; failures=$((failures + 1)); } | ||
|
|
||
| touch "$STUB_DIR/list-fails" | ||
| run "sends every file when the listing fails" 0 "" "--no-realm-events" | ||
| rm "$STUB_DIR/list-fails" | ||
|
|
||
| deployments | ||
| succeeded | ||
| KNOWN_REVISION="$a" run "a known revision stands in for a success" 0 \ | ||
| "$a" "--changed-since $a --no-realm-events" | ||
|
|
||
| deployments 3:"$c" 2:"$b" | ||
| succeeded | ||
| KNOWN_REVISION="$a" run "a known revision adds the deployments that never succeeded" 0 \ | ||
| "$a $c $b" "--changed-since $a --changed-since $c --changed-since $b --no-realm-events" | ||
|
|
||
| deployments 3:"$c" 2:"$b" | ||
| succeeded 2 | ||
| cli_help "" | ||
| run "an older boxel-cli gets neither option" 0 "$c $b" "" | ||
| grep -q 'no --changed-since' "$work/log" || { echo "FAIL: no notice for a CLI without --changed-since"; failures=$((failures + 1)); } | ||
| grep -q 'no --no-realm-events' "$work/log" || { echo "FAIL: no notice for a CLI without --no-realm-events"; failures=$((failures + 1)); } | ||
|
|
||
| cli_help " --changed-since <revision>" | ||
| run "a boxel-cli without --no-realm-events still skips unchanged files" 0 \ | ||
| "$c $b" "--changed-since $c --changed-since $b" | ||
|
|
||
| cli_help "$both_flags" | ||
| deployments 2:"$c" | ||
| succeeded | ||
| REQUIRE_REVISIONS=true run "require-revisions refuses to send every file" 1 "" "" | ||
|
|
||
| deployments 3:"$c" 2:"$b" | ||
| succeeded 2 | ||
| cli_help " --no-realm-events" | ||
| REQUIRE_REVISIONS=true run "require-revisions refuses a boxel-cli without --changed-since" 1 "" "" | ||
|
|
||
| # PATH without the boxel stub, or any installed boxel. git and the shell | ||
| # utilities come from the system directories. | ||
| mkdir -p "$STUB_DIR/no-boxel" | ||
| ln -s "$STUB_DIR/bin/gh" "$STUB_DIR/no-boxel/gh" | ||
| deployments 3:"$c" 2:"$b" | ||
| succeeded 2 | ||
| PATH="$STUB_DIR/no-boxel:/usr/bin:/bin" run "a missing boxel-cli fails before anything is recorded" 1 "" "" | ||
| grep -q "boxel isn't on PATH" "$work/log" || { echo "FAIL: no error for a missing boxel-cli"; failures=$((failures + 1)); } | ||
|
|
||
| if [ "$failures" -ne 0 ]; then | ||
| echo "$failures failure(s)" | ||
| exit 1 | ||
| fi |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code 🤖] When
boxelisn't on PATH, this probe treats it as an old CLI.push-argscomes out empty and the step still succeeds, so a staging sync sends every file, with a notice that blames the CLI version. Underrequire-revisions, the refusal reads "No revision to compare against", which isn't the real cause either. I ran start.sh withboxelmissing from PATH: it exits 0 withpush-args=empty, and withREQUIRE_REVISIONS=trueit exits 1 with that error.The only thing guarding this is the "after … the boxel-cli install" line in
start/action.yml. Both catalog workflows,sync-to-workspace.ymlanddeploy-production.yml, record their deployment before theirInstall Boxel CLIstep. A step-for-step move onto this action lands right here: staging re-sends the whole catalog, and production refuses with a misleading message.Please fail fast at the top of start.sh, before anything is listed or recorded:
Please also add a start.test.sh case that removes the
boxelstub. (Checking the exit status of--helpwouldn't work here: it would turn the intended old-CLI fallback into a failure.)Regression in the new action. Non-blocking:
sync-workspace.ymlalready installs the CLI beforestart.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code 🤖] Fixed in
54d8c29.start.shnow checkscommand -v boxelbefore it lists or records anything. A missing CLI fails the step with "boxel isn't on PATH. Run realm-deployment/start after installing @cardstack/boxel-cli." It no longer falls through to the older-CLI path.start.test.shhas a case that runs it on a PATH with noboxeland expects exit 1 with that error.