77 description : >-
88 auto — default release-please behavior (publish when a release PR was merged).
99 release-pr — open or refresh the release PR only; merge it yourself to publish.
10- force — bump version from current main and publish ( for dependency-only changes) .
10+ force — open a Release-As marker PR for dependency-only changes.
1111 required : true
1212 default : auto
1313 type : choice
@@ -61,29 +61,77 @@ jobs:
6161 echo "value=auto" >> "$GITHUB_OUTPUT"
6262 fi
6363
64- - name : Prepare forced release commit
64+ - name : Resolve forced release version
6565 if : ${{ steps.mode.outputs.value == 'force' }}
6666 id : force
6767 env :
6868 BUMP : ${{ inputs.bump || 'patch' }}
6969 run : |
70- CURRENT=$(jq -r '."packages/grove-cli"' .release-please-manifest.json)
71- IFS=. read -r MAJOR MINOR PATCH <<< "$CURRENT"
72- case "$BUMP" in
73- major) NEXT="$((MAJOR + 1)).0.0" ;;
74- minor) NEXT="${MAJOR}.$((MINOR + 1)).0" ;;
75- patch) NEXT="${MAJOR}.${MINOR}.$((PATCH + 1))" ;;
76- *) echo "Unsupported bump: $BUMP" >&2; exit 1 ;;
77- esac
70+ NEXT=$(node --input-type=module <<'NODE'
71+ import { readFileSync } from "node:fs";
72+
73+ const bump = process.env.BUMP;
74+ const manifest = JSON.parse(readFileSync(".release-please-manifest.json", "utf8"));
75+ const versions = Object.values(manifest).map((version) => {
76+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
77+ if (!match) throw new Error(`Unsupported manifest version: ${version}`);
78+ return match.slice(1).map(Number);
79+ });
80+ const [major, minor, patch] = versions.reduce((max, version) => {
81+ for (let i = 0; i < 3; i += 1) {
82+ if (version[i] > max[i]) return version;
83+ if (version[i] < max[i]) return max;
84+ }
85+ return max;
86+ });
87+
88+ if (bump === "major") console.log(`${major + 1}.0.0`);
89+ else if (bump === "minor") console.log(`${major}.${minor + 1}.0`);
90+ else if (bump === "patch") console.log(`${major}.${minor}.${patch + 1}`);
91+ else throw new Error(`Unsupported bump: ${bump}`);
92+ NODE
93+ )
7894 echo "version=$NEXT" >> "$GITHUB_OUTPUT"
95+ echo "Resolved forced release version $NEXT"
96+
97+ - name : Open forced release marker PR
98+ if : ${{ steps.mode.outputs.value == 'force' }}
99+ env :
100+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
101+ NEXT : ${{ steps.force.outputs.version }}
102+ run : |
103+ set -euo pipefail
104+ BRANCH="release-force/${NEXT}"
105+
79106 git config user.name "github-actions[bot]"
80107 git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
81- git commit --allow-empty -m "chore: force release ${NEXT}" -m "Release-As: ${NEXT}"
82- git push origin HEAD:main
83- echo "Prepared Release-As $NEXT"
108+
109+ if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
110+ git fetch origin "$BRANCH"
111+ git checkout -B "$BRANCH" "origin/$BRANCH"
112+ else
113+ git checkout -B "$BRANCH"
114+ git commit --allow-empty -m "chore: force release ${NEXT}" -m "Release-As: ${NEXT}"
115+ git push --set-upstream origin "$BRANCH"
116+ fi
117+
118+ BODY=$(cat <<EOF
119+ Opens a protected-branch compliant Release-As marker.
120+
121+ After this PR is merged, the normal push workflow will open the release-please PR for ${NEXT}. Merge that release PR to publish to npm.
122+ EOF
123+ )
124+
125+ PR=$(gh pr list --head "$BRANCH" --base main --json number --jq '.[0].number')
126+ if [ -z "$PR" ] || [ "$PR" = "null" ]; then
127+ gh pr create --base main --head "$BRANCH" --title "chore: force release ${NEXT}" --body "$BODY"
128+ else
129+ gh pr edit "$PR" --title "chore: force release ${NEXT}" --body "$BODY"
130+ echo "Updated existing force release PR #$PR"
131+ fi
84132
85133 - name : Open or refresh release PR
86- if : ${{ steps.mode.outputs.value == 'force' || steps.mode.outputs.value == ' release-pr' }}
134+ if : ${{ steps.mode.outputs.value == 'release-pr' }}
87135 uses : googleapis/release-please-action@v5
88136 id : release_pr
89137 with :
@@ -92,28 +140,14 @@ jobs:
92140 manifest-file : .release-please-manifest.json
93141 skip-github-release : true
94142
95- - name : Merge release PR
96- if : ${{ steps.mode.outputs.value == 'force' }}
97- env :
98- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
99- run : |
100- PR=$(gh pr list --head release-please--branches--main --base main --json number --jq '.[0].number')
101- if [ -z "$PR" ] || [ "$PR" = "null" ]; then
102- echo "No release PR found to merge" >&2
103- exit 1
104- fi
105- gh pr merge "$PR" --merge --delete-branch
106- git pull origin main
107-
108143 - name : Release Please
109- if : ${{ steps.mode.outputs.value != 'release-pr ' }}
144+ if : ${{ steps.mode.outputs.value == 'auto ' }}
110145 uses : googleapis/release-please-action@v5
111146 id : release
112147 with :
113148 token : ${{ secrets.GITHUB_TOKEN }}
114149 config-file : release-please-config.json
115150 manifest-file : .release-please-manifest.json
116- skip-github-pull-request : ${{ steps.mode.outputs.value == 'force' }}
117151
118152 - name : Install Dependencies
119153 if : ${{ steps.release.outputs.releases_created == 'true' }}
@@ -129,6 +163,52 @@ jobs:
129163
130164 - name : Publish to NPM
131165 if : ${{ steps.release.outputs.releases_created == 'true' }}
132- run : pnpm -r publish --access public
133166 env :
134167 NPM_CONFIG_PROVENANCE : true
168+ PATHS_RELEASED : ${{ steps.release.outputs.paths_released }}
169+ run : |
170+ set -euo pipefail
171+ node --input-type=module <<'NODE' > /tmp/released-packages
172+ const paths = JSON.parse(process.env.PATHS_RELEASED || "[]");
173+ for (const path of paths) console.log(path);
174+ NODE
175+
176+ if [ ! -s /tmp/released-packages ]; then
177+ echo "No released package paths reported by release-please" >&2
178+ exit 1
179+ fi
180+
181+ while IFS= read -r path; do
182+ echo "Publishing ${path}"
183+ pnpm --filter "./${path}" publish --access public --no-git-checks
184+ done < /tmp/released-packages
185+
186+ - name : Verify NPM dist-tags
187+ if : ${{ steps.release.outputs.releases_created == 'true' }}
188+ env :
189+ PATHS_RELEASED : ${{ steps.release.outputs.paths_released }}
190+ run : |
191+ node --input-type=module <<'NODE'
192+ import { execFileSync } from "node:child_process";
193+ import { readFileSync } from "node:fs";
194+ import { join } from "node:path";
195+
196+ const paths = JSON.parse(process.env.PATHS_RELEASED || "[]");
197+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
198+
199+ for (const path of paths) {
200+ const pkg = JSON.parse(readFileSync(join(path, "package.json"), "utf8"));
201+ let latest = "";
202+ for (let attempt = 1; attempt <= 6; attempt += 1) {
203+ latest = execFileSync("npm", ["view", `${pkg.name}@latest`, "version"], {
204+ encoding: "utf8",
205+ }).trim();
206+ if (latest === pkg.version) break;
207+ await sleep(10000);
208+ }
209+ if (latest !== pkg.version) {
210+ throw new Error(`${pkg.name}@latest is ${latest}, expected ${pkg.version}`);
211+ }
212+ console.log(`${pkg.name}@latest -> ${latest}`);
213+ }
214+ NODE
0 commit comments