Skip to content

Commit 781d718

Browse files
committed
fix(release): validate recovery refs before publish
1 parent cd1bd0c commit 781d718

2 files changed

Lines changed: 48 additions & 17 deletions

File tree

.github/workflows/release.yml

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,23 @@ jobs:
5858
config-file: release-please-config.json
5959
manifest-file: .release-please-manifest.json
6060

61-
publish:
62-
name: Publish to Hex
61+
recovery-validation:
62+
name: Validate Recovery Ref
63+
if: ${{ github.event_name == 'workflow_dispatch' }}
6364
runs-on: ubuntu-latest
64-
needs: release-please
65-
if: ${{ github.event_name == 'workflow_dispatch' || needs.release-please.outputs.release_created == 'true' }}
66-
environment: hex-publish
6765
permissions:
6866
contents: read
67+
outputs:
68+
checkout_ref: ${{ steps.validate.outputs.checkout_ref }}
6969
steps:
70-
- name: Check out repository for the merged release commit
71-
if: ${{ github.event_name != 'workflow_dispatch' }}
72-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
73-
7470
- name: Check out repository for recovery validation
75-
if: ${{ github.event_name == 'workflow_dispatch' }}
7671
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
7772
with:
7873
fetch-depth: 0
7974
fetch-tags: true
8075

8176
- name: Validate recovery-only inputs and lock to an immutable ref
82-
if: ${{ github.event_name == 'workflow_dispatch' }}
77+
id: validate
8378
shell: bash
8479
run: |
8580
set -euo pipefail
@@ -89,16 +84,47 @@ jobs:
8984
9085
if [[ "$recovery_ref" =~ ^[0-9a-f]{40}$ ]]; then
9186
git cat-file -e "${recovery_ref}^{commit}"
92-
git checkout --detach "$recovery_ref"
87+
echo "checkout_ref=$recovery_ref" >> "$GITHUB_OUTPUT"
9388
elif git show-ref --verify --quiet "refs/tags/$recovery_ref"; then
94-
git checkout --detach "refs/tags/$recovery_ref"
89+
echo "checkout_ref=$recovery_ref" >> "$GITHUB_OUTPUT"
9590
else
9691
echo "workflow_dispatch is recovery-only and recovery_ref must be an exact 40-character commit SHA or an existing tag."
9792
exit 1
9893
fi
9994
10095
echo "workflow_dispatch is recovery-only. Recovery publishes the exact immutable ref selected in recovery_ref, and normal publish intent starts from a merged Release Please PR and the protected hex-publish environment."
10196
97+
publish:
98+
name: Publish to Hex
99+
runs-on: ubuntu-latest
100+
needs:
101+
- release-please
102+
- recovery-validation
103+
if: ${{ always() && ((github.event_name == 'workflow_dispatch' && needs.recovery-validation.result == 'success') || (github.event_name != 'workflow_dispatch' && needs.release-please.outputs.release_created == 'true')) }}
104+
environment: hex-publish
105+
permissions:
106+
contents: read
107+
steps:
108+
- name: Check out repository for the merged release commit
109+
if: ${{ github.event_name != 'workflow_dispatch' }}
110+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
111+
112+
- name: Check out repository for recovery validation
113+
if: ${{ github.event_name == 'workflow_dispatch' }}
114+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
115+
with:
116+
fetch-depth: 0
117+
fetch-tags: true
118+
ref: ${{ needs.recovery-validation.outputs.checkout_ref }}
119+
120+
- name: Confirm recovery checkout is detached to the validated immutable ref
121+
if: ${{ github.event_name == 'workflow_dispatch' }}
122+
shell: bash
123+
run: |
124+
set -euo pipefail
125+
git checkout --detach HEAD
126+
echo "Recovered immutable ref: ${{ needs.recovery-validation.outputs.checkout_ref }}"
127+
102128
- name: Set up Elixir and Erlang
103129
uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
104130
with:

test/lockspire/release_readiness_contract_test.exs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,20 @@ defmodule Lockspire.ReleaseReadinessContractTest do
5959
assert release_workflow =~ "steps.manual_dispatch.outputs.release_created"
6060
assert release_workflow =~ "echo \"release_created=false\" >> \"$GITHUB_OUTPUT\""
6161
assert release_workflow =~ "workflow_dispatch bypasses Release Please"
62+
assert release_workflow =~ "recovery-validation:"
63+
assert release_workflow =~ "name: Validate Recovery Ref"
6264
assert release_workflow =~ "Check out repository for recovery validation"
6365
assert release_workflow =~ "fetch-depth: 0"
6466
assert release_workflow =~ "fetch-tags: true"
6567
assert release_workflow =~ "Validate recovery-only inputs and lock to an immutable ref"
6668
assert release_workflow =~ "[[ \"$recovery_ref\" =~ ^[0-9a-f]{40}$ ]]"
6769
assert release_workflow =~ "git show-ref --verify --quiet \"refs/tags/$recovery_ref\""
68-
assert release_workflow =~ "git checkout --detach \"$recovery_ref\""
69-
assert release_workflow =~ "git checkout --detach \"refs/tags/$recovery_ref\""
70+
assert release_workflow =~ "echo \"checkout_ref=$recovery_ref\" >> \"$GITHUB_OUTPUT\""
7071
assert release_workflow =~ "exact 40-character commit SHA or an existing tag"
7172
assert release_workflow =~ "workflow_dispatch is recovery-only"
73+
assert release_workflow =~ "ref: ${{ needs.recovery-validation.outputs.checkout_ref }}"
74+
assert release_workflow =~ "Confirm recovery checkout is detached to the validated immutable ref"
75+
assert release_workflow =~ "git checkout --detach HEAD"
7276
assert release_workflow =~ "Release Please generated PRs are review-only"
7377
assert release_workflow =~ "id: release"
7478
assert release_workflow =~ "github.event_name == 'workflow_dispatch'"
@@ -86,8 +90,9 @@ defmodule Lockspire.ReleaseReadinessContractTest do
8690
assert release_workflow =~ "run: mix release.preflight"
8791
assert release_workflow =~ "run: mix hex.publish --yes"
8892

89-
assert release_workflow =~
90-
"if: ${{ github.event_name == 'workflow_dispatch' || needs.release-please.outputs.release_created == 'true' }}"
93+
assert release_workflow =~ "needs.recovery-validation.result == 'success'"
94+
assert release_workflow =~ "needs.release-please.outputs.release_created == 'true'"
95+
assert release_workflow =~ "always()"
9196

9297
refute release_workflow =~ "pull_request:"
9398
refute release_workflow =~ "package-name: lockspire"

0 commit comments

Comments
 (0)