From cb7b731f8ca45fab34e2c30f61e8388ba344b682 Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Mon, 22 Jun 2026 16:45:28 +0700 Subject: [PATCH 1/5] feat: expose written metadata as write action outputs Add `data` (post-merge stringified value) and `changed` (whether the PR body was updated) outputs to the write action, mirroring how read exposes its result. Document them in the README and assert them in the write self-test workflow. --- .github/workflows/tests-write.yaml | 30 ++++++++++++++++++++++++++++++ README.md | 13 ++++++++++++- write/action.yml | 15 ++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-write.yaml b/.github/workflows/tests-write.yaml index f67a239..a90308c 100644 --- a/.github/workflows/tests-write.yaml +++ b/.github/workflows/tests-write.yaml @@ -54,3 +54,33 @@ jobs: [ "$(echo "$RESULT" | jq -r '.author')" = "test" ] || { echo "::error::author not added"; exit 1; } echo "write self-test passed" + + # The write action exposes what it persisted as `data`/`changed` outputs. + - name: Assert write outputs + env: + SEED_DATA: ${{ steps.seed.outputs.data }} + SEED_CHANGED: ${{ steps.seed.outputs.changed }} + MERGE_DATA: ${{ steps.merge.outputs.data }} + MERGE_CHANGED: ${{ steps.merge.outputs.changed }} + RESULT: ${{ steps.read.outputs.result }} + run: | + echo "seed.data=$SEED_DATA seed.changed=$SEED_CHANGED" + echo "merge.data=$MERGE_DATA merge.changed=$MERGE_CHANGED" + + # Both writes mutate the body, so changed must be true. + [ "$SEED_CHANGED" = "true" ] || { echo "::error::seed.changed not true"; exit 1; } + [ "$MERGE_CHANGED" = "true" ] || { echo "::error::merge.changed not true"; exit 1; } + + # Replace output reflects exactly what was passed in. + [ "$(echo "$SEED_DATA" | jq -r '.version')" = "1.2.3" ] || { echo "::error::seed.data version wrong"; exit 1; } + [ "$(echo "$SEED_DATA" | jq -r '.reviewed')" = "false" ] || { echo "::error::seed.data reviewed wrong"; exit 1; } + + # Merge output reflects the post-merge value (old ∪ new). + [ "$(echo "$MERGE_DATA" | jq -r '.version')" = "1.2.3" ] || { echo "::error::merge.data version not preserved"; exit 1; } + [ "$(echo "$MERGE_DATA" | jq -r '.reviewed')" = "true" ] || { echo "::error::merge.data reviewed not merged"; exit 1; } + [ "$(echo "$MERGE_DATA" | jq -r '.author')" = "test" ] || { echo "::error::merge.data author not added"; exit 1; } + + # The merge output must match what a subsequent read sees. + [ "$(echo "$MERGE_DATA" | jq -S .)" = "$(echo "$RESULT" | jq -S .)" ] || { echo "::error::merge.data does not match read result"; exit 1; } + + echo "write outputs self-test passed" diff --git a/README.md b/README.md index 6dbfeeb..db57e26 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,15 @@ permissions: When `merge` is `true`, both the existing and incoming values must be plain JSON objects. +### Outputs + +| Name | Description | +| --------- | --------------------------------------------------------------------------------- | +| `data` | JSON-stringified metadata that was written, after the merge if `merge` is enabled | +| `changed` | `"true"` if the PR body was updated, `"false"` if it was already up to date | + +`data` is the effective stored value, which is the most reliable way to learn the result of a `merge: "true"` write without reading the body back. + ### Example ```yaml @@ -81,10 +90,12 @@ jobs: write-metadata: runs-on: ubuntu-latest steps: - - uses: taskworld/pr-meta-action/write@v1 + - id: meta + uses: taskworld/pr-meta-action/write@v1 with: data: '{"reviewed":true,"version":"1.2.3"}' merge: "true" + - run: echo 'wrote ${{ steps.meta.outputs.data }} (changed=${{ steps.meta.outputs.changed }})' ``` ## Notes diff --git a/write/action.yml b/write/action.yml index a923a29..cf3a9fb 100644 --- a/write/action.yml +++ b/write/action.yml @@ -21,6 +21,14 @@ inputs: default: "false" required: false +outputs: + data: + description: JSON-stringified metadata that was written (after merge, if enabled) + value: ${{ steps.write.outputs.data }} + changed: + description: true when the PR body was updated, false when it was already up to date + value: ${{ steps.write.outputs.changed }} + runs: using: composite steps: @@ -95,7 +103,9 @@ runs: block : `${body}\n\n${block}\n` - if (newBody !== body) { + const changed = newBody !== body + + if (changed) { await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, @@ -103,3 +113,6 @@ runs: body: newBody, }) } + + core.setOutput('data', JSON.stringify(newVal)) + core.setOutput('changed', String(changed)) From 22db17ff3104e14cca2300c6da1d589e4a0324bd Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Mon, 22 Jun 2026 16:50:51 +0700 Subject: [PATCH 2/5] fix: serialize self-test workflows to avoid PR-body write race tests-read and tests-write each do a whole-body read-modify-write on the same PR description. Running on separate pull_request triggers made them race and clobber each other's seeded markers. Convert both to reusable workflow_call workflows and add a tests orchestrator that chains them with `needs` so they run sequentially. --- .github/workflows/tests-read.yaml | 2 +- .github/workflows/tests-write.yaml | 2 +- .github/workflows/tests.yaml | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests-read.yaml b/.github/workflows/tests-read.yaml index 2db06a4..7318759 100644 --- a/.github/workflows/tests-read.yaml +++ b/.github/workflows/tests-read.yaml @@ -1,7 +1,7 @@ name: tests-read on: - pull_request: + workflow_call: # read only needs to read; the seed step uses write, which needs write. permissions: diff --git a/.github/workflows/tests-write.yaml b/.github/workflows/tests-write.yaml index a90308c..acce51a 100644 --- a/.github/workflows/tests-write.yaml +++ b/.github/workflows/tests-write.yaml @@ -1,7 +1,7 @@ name: tests-write on: - pull_request: + workflow_call: # write updates the PR body via the REST API permissions: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..058f7b2 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,20 @@ +name: tests + +on: + pull_request: + +# Both reusable workflows seed the PR body via the REST API. +permissions: + pull-requests: write + contents: read + +# Run the two suites as a chain rather than in parallel: each one does a +# whole-body read-modify-write on the same PR description, so running them +# concurrently makes their writes clobber each other. `needs` serializes them. +jobs: + read: + uses: ./.github/workflows/tests-read.yaml + + write: + needs: read + uses: ./.github/workflows/tests-write.yaml From fbc1433f29be10d8ccdf4f5ad703f6607da6ee5e Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Mon, 22 Jun 2026 17:00:57 +0700 Subject: [PATCH 3/5] test: add settle delay before reading back seeded PR metadata pulls.update is not immediately consistent, so a pulls.get a couple of seconds later can hit a stale replica and miss the just-seeded marker. Sleep 10s before each read-back (and before the merge step, which also re-reads the body) so the self-tests stop flaking on API propagation lag. --- .github/workflows/tests-read.yaml | 5 +++++ .github/workflows/tests-write.yaml | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/tests-read.yaml b/.github/workflows/tests-read.yaml index 7318759..3d23062 100644 --- a/.github/workflows/tests-read.yaml +++ b/.github/workflows/tests-read.yaml @@ -21,6 +21,11 @@ jobs: marker: test-read data: '{"version":"4.5.6","reviewed":true}' + # The seed's pulls.update is not immediately consistent: a pulls.get + # right after it can hit a replica that hasn't caught up. Give it a + # moment to propagate before reading back. + - run: sleep 10 + # Read via an explicit pr-number so the action takes the REST fetch path. # The default path reads the event payload's PR body, which is captured # when the run starts and so can't see the seed written above this run. diff --git a/.github/workflows/tests-write.yaml b/.github/workflows/tests-write.yaml index acce51a..5033e4b 100644 --- a/.github/workflows/tests-write.yaml +++ b/.github/workflows/tests-write.yaml @@ -21,6 +21,11 @@ jobs: marker: test-write data: '{"version":"1.2.3","reviewed":false}' + # pulls.update is not immediately consistent, and the merge step below + # reads the body back to find the seeded marker. Let the seed propagate + # first, otherwise merge can miss it and append a duplicate block. + - run: sleep 10 + # Merge: shallow-merge new keys into the existing blob (merge=true). - id: merge uses: ./write @@ -29,6 +34,9 @@ jobs: data: '{"reviewed":true,"author":"test"}' merge: "true" + # Same propagation delay before reading the merged result back. + - run: sleep 10 + # Read it back to assert the round-trip survived the composite-input plumbing. # Explicit pr-number forces the REST fetch path; the default path reads the # event payload body captured at run start, before the writes above. From a3f83996a5439719cabef4b598a4a35e29e2d0e7 Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Wed, 24 Jun 2026 11:33:03 +0700 Subject: [PATCH 4/5] refactor: rename write output `data` to `result` for consistency with read --- .github/workflows/tests-write.yaml | 22 +++++++++++----------- README.md | 6 +++--- write/action.yml | 6 +++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests-write.yaml b/.github/workflows/tests-write.yaml index 5033e4b..800531c 100644 --- a/.github/workflows/tests-write.yaml +++ b/.github/workflows/tests-write.yaml @@ -63,32 +63,32 @@ jobs: echo "write self-test passed" - # The write action exposes what it persisted as `data`/`changed` outputs. + # The write action exposes what it persisted as `result`/`changed` outputs. - name: Assert write outputs env: - SEED_DATA: ${{ steps.seed.outputs.data }} + SEED_RESULT: ${{ steps.seed.outputs.result }} SEED_CHANGED: ${{ steps.seed.outputs.changed }} - MERGE_DATA: ${{ steps.merge.outputs.data }} + MERGE_RESULT: ${{ steps.merge.outputs.result }} MERGE_CHANGED: ${{ steps.merge.outputs.changed }} RESULT: ${{ steps.read.outputs.result }} run: | - echo "seed.data=$SEED_DATA seed.changed=$SEED_CHANGED" - echo "merge.data=$MERGE_DATA merge.changed=$MERGE_CHANGED" + echo "seed.result=$SEED_RESULT seed.changed=$SEED_CHANGED" + echo "merge.result=$MERGE_RESULT merge.changed=$MERGE_CHANGED" # Both writes mutate the body, so changed must be true. [ "$SEED_CHANGED" = "true" ] || { echo "::error::seed.changed not true"; exit 1; } [ "$MERGE_CHANGED" = "true" ] || { echo "::error::merge.changed not true"; exit 1; } # Replace output reflects exactly what was passed in. - [ "$(echo "$SEED_DATA" | jq -r '.version')" = "1.2.3" ] || { echo "::error::seed.data version wrong"; exit 1; } - [ "$(echo "$SEED_DATA" | jq -r '.reviewed')" = "false" ] || { echo "::error::seed.data reviewed wrong"; exit 1; } + [ "$(echo "$SEED_RESULT" | jq -r '.version')" = "1.2.3" ] || { echo "::error::seed.result version wrong"; exit 1; } + [ "$(echo "$SEED_RESULT" | jq -r '.reviewed')" = "false" ] || { echo "::error::seed.result reviewed wrong"; exit 1; } # Merge output reflects the post-merge value (old ∪ new). - [ "$(echo "$MERGE_DATA" | jq -r '.version')" = "1.2.3" ] || { echo "::error::merge.data version not preserved"; exit 1; } - [ "$(echo "$MERGE_DATA" | jq -r '.reviewed')" = "true" ] || { echo "::error::merge.data reviewed not merged"; exit 1; } - [ "$(echo "$MERGE_DATA" | jq -r '.author')" = "test" ] || { echo "::error::merge.data author not added"; exit 1; } + [ "$(echo "$MERGE_RESULT" | jq -r '.version')" = "1.2.3" ] || { echo "::error::merge.result version not preserved"; exit 1; } + [ "$(echo "$MERGE_RESULT" | jq -r '.reviewed')" = "true" ] || { echo "::error::merge.result reviewed not merged"; exit 1; } + [ "$(echo "$MERGE_RESULT" | jq -r '.author')" = "test" ] || { echo "::error::merge.result author not added"; exit 1; } # The merge output must match what a subsequent read sees. - [ "$(echo "$MERGE_DATA" | jq -S .)" = "$(echo "$RESULT" | jq -S .)" ] || { echo "::error::merge.data does not match read result"; exit 1; } + [ "$(echo "$MERGE_RESULT" | jq -S .)" = "$(echo "$RESULT" | jq -S .)" ] || { echo "::error::merge.result does not match read result"; exit 1; } echo "write outputs self-test passed" diff --git a/README.md b/README.md index db57e26..2e4eea6 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,10 @@ When `merge` is `true`, both the existing and incoming values must be plain JSON | Name | Description | | --------- | --------------------------------------------------------------------------------- | -| `data` | JSON-stringified metadata that was written, after the merge if `merge` is enabled | +| `result` | JSON-stringified metadata that was written, after the merge if `merge` is enabled | | `changed` | `"true"` if the PR body was updated, `"false"` if it was already up to date | -`data` is the effective stored value, which is the most reliable way to learn the result of a `merge: "true"` write without reading the body back. +`result` is the effective stored value, which is the most reliable way to learn the result of a `merge: "true"` write without reading the body back. ### Example @@ -95,7 +95,7 @@ jobs: with: data: '{"reviewed":true,"version":"1.2.3"}' merge: "true" - - run: echo 'wrote ${{ steps.meta.outputs.data }} (changed=${{ steps.meta.outputs.changed }})' + - run: echo 'wrote ${{ steps.meta.outputs.result }} (changed=${{ steps.meta.outputs.changed }})' ``` ## Notes diff --git a/write/action.yml b/write/action.yml index cf3a9fb..8eee880 100644 --- a/write/action.yml +++ b/write/action.yml @@ -22,9 +22,9 @@ inputs: required: false outputs: - data: + result: description: JSON-stringified metadata that was written (after merge, if enabled) - value: ${{ steps.write.outputs.data }} + value: ${{ steps.write.outputs.result }} changed: description: true when the PR body was updated, false when it was already up to date value: ${{ steps.write.outputs.changed }} @@ -114,5 +114,5 @@ runs: }) } - core.setOutput('data', JSON.stringify(newVal)) + core.setOutput('result', JSON.stringify(newVal)) core.setOutput('changed', String(changed)) From a7a6b5fe3e3512db6c43b56053595c4f90169c2c Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Wed, 24 Jun 2026 15:10:33 +0700 Subject: [PATCH 5/5] chore: re-trigger CI