Skip to content

Commit 57c261e

Browse files
vahidkay-metameta-codesync[bot]
authored andcommitted
Release workflow fixes: changelog gen, auto-build, set-stable-tag lookup, build/ packaging (#175)
Summary: Release-workflow fixes, aligning this repo with `facebook-for-woocommerce` and fixing bugs surfaced while cutting 5.2.2. 1. **Changelog generation** (`prepare-release.yml`) — fbshipit closes PRs (`mergedAt=null` + `Merged` label), which the old builder skipped → empty changelogs. Now detects landed PRs via `Merged` label / `merged_at`, `closed_at` date fallback, tag-commit-timestamp cutoff; keeps every `changelog:*` except `none`; skips `Release X.Y.Z` PRs. 2. **Auto-build on prepare** — `build-and-upload.yml` gains `workflow_call` (with `ref`); `prepare-release.yml` calls it inline **and** dispatches a standalone run (for `set-stable-tag` to find). Adds `actions: write` + `version` output. 3. **Woo safeguards** — previous-release-PR guard, `include-hidden-files: true`, per-PR builds. 4. **`set-stable-tag.yml` artifact lookup** — matched runs by `displayTitle` (never contains the branch ref) → "No build found". Now matches by `--branch` + `headSha` with polling. (Previously fixed only on `release/p5.2.1/publish`, never reached `main`.) 5. **Stray `build/` dir in the package** (`build.xml`) — the sourcedir fileset copied `.` into `./build/official-facebook-pixel/` without excluding `build/`, shipping a nested empty `build/`. Invisible because empty dirs are dropped by `upload-artifact` but committed by `svn add`. Excluded `build` + `build/**`. 6. **`pull-requests: read`** on the changelog step (else `gh pr list` 403s under the explicit permissions block) + **fail on empty changelog** (both incorporated from #170, which this supersedes). 7. **Release-notes extraction + spacing** — the `release-plugin.yml` "Extract release notes" loop broke at the current version's own header before collecting bullets, so the GitHub Release body (and the release PR body, copied from it) was always empty; fixed to collect the first block's bullets. Also normalized generated changelog/readme to a single blank line between release blocks. Supersedes #170. Pull Request resolved: #175 Reviewed By: rafael-curran Differential Revision: D113760571 Pulled By: vahidkay-meta fbshipit-source-id: b1d6c6bd089d2bf8312a5a10e971562e32c0b58e
1 parent fffff8c commit 57c261e

5 files changed

Lines changed: 253 additions & 89 deletions

File tree

.github/workflows/build-and-upload.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ on:
44
push:
55
branches:
66
- 'release/**'
7+
pull_request:
8+
types: [opened, synchronize, reopened]
79
workflow_dispatch:
10+
workflow_call:
11+
inputs:
12+
ref:
13+
description: "Git ref (branch/tag/sha) to build. Defaults to the triggering ref."
14+
required: false
15+
type: string
816

917
permissions:
1018
contents: read
19+
pull-requests: write # to post the build download link on PRs
1120

1221
concurrency:
1322
group: ${{ github.workflow }}-${{ github.ref }}
@@ -19,6 +28,8 @@ jobs:
1928
steps:
2029
- name: Checkout release branch
2130
uses: actions/checkout@v6
31+
with:
32+
ref: ${{ inputs.ref || github.ref }}
2233

2334
- name: Setup PHP
2435
uses: shivammathur/setup-php@v2
@@ -45,3 +56,35 @@ jobs:
4556
with:
4657
name: official-facebook-pixel
4758
path: artifact-contents
59+
# Dependencies ship dotfiles (e.g. .babelrc / .eslintrc.json).
60+
# upload-artifact excludes hidden files by default, which makes the
61+
# artifact diverge from the deployed plugin and trips the Set Stable
62+
# Tag comparison. Keep them so the artifact is faithful.
63+
include-hidden-files: true
64+
65+
- name: Post build download link on PR
66+
if: github.event_name == 'pull_request'
67+
continue-on-error: true # fork PRs get a read-only token; skip quietly
68+
env:
69+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70+
PR_NUMBER: ${{ github.event.pull_request.number }}
71+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
72+
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
73+
run: |
74+
MARKER="<!-- plugin-build-artifact -->"
75+
BODY="$MARKER
76+
## 📦 Latest plugin build
77+
78+
**Commit:** ${COMMIT_SHA:0:7}
79+
**Download:** [\`official-facebook-pixel\` artifact](${RUN_URL}) — open the run and scroll to the bottom.
80+
81+
_To install: download → unzip → upload via Plugins › Add New Plugin › Upload Plugin._"
82+
83+
# Update the existing build comment in place (idempotent via the marker).
84+
EXISTING_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
85+
--paginate --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" | head -1)
86+
if [ -n "$EXISTING_ID" ]; then
87+
gh api "repos/${{ github.repository }}/issues/comments/${EXISTING_ID}" -X PATCH -f body="$BODY"
88+
else
89+
gh pr comment "$PR_NUMBER" --body "$BODY"
90+
fi

.github/workflows/prepare-release.yml

Lines changed: 142 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ on:
1010

1111
permissions:
1212
contents: write
13+
pull-requests: read # required for `gh pr list` in the changelog step
14+
actions: write # required to dispatch build-and-upload.yml for the new release branch
1315

1416
concurrency:
1517
group: ${{ github.workflow }}-${{ github.event.inputs.version }}
@@ -19,6 +21,8 @@ jobs:
1921
prepare-release:
2022
name: Prepare Release
2123
runs-on: ubuntu-latest
24+
outputs:
25+
version: ${{ steps.set_version.outputs.new_version }}
2226
steps:
2327
- name: Checkout repository
2428
uses: actions/checkout@v6
@@ -63,86 +67,122 @@ jobs:
6367
}
6468
}
6569
70+
- name: Ensure previous release PR is merged
71+
env:
72+
GH_TOKEN: ${{ github.token }}
73+
NEW_VERSION: ${{ steps.set_version.outputs.new_version }}
74+
run: |
75+
set -euo pipefail
76+
77+
# Each release opens a PR ("Release X.Y.Z", head release/X.Y.Z -> main)
78+
# that is merged once the release is published. Don't start a new
79+
# release while a previous release PR is still open, otherwise releases
80+
# overlap and the changelog/version bookkeeping drifts. Fail fast if any
81+
# unmerged release PR (other than this version's) is still open.
82+
if ! OPEN_RELEASE_PRS=$(gh pr list \
83+
--repo "${{ github.repository }}" \
84+
--base main \
85+
--state open \
86+
--json number,headRefName,title,url); then
87+
echo "::error::Failed to query open PRs via the GitHub API; cannot verify previous release PRs are merged."
88+
exit 1
89+
fi
90+
91+
# An empty/non-JSON response would make the filter below silently pass
92+
# and bypass the guard, so validate we got a JSON array back first.
93+
if ! echo "$OPEN_RELEASE_PRS" | jq -e 'type == "array"' >/dev/null 2>&1; then
94+
echo "::error::Unexpected response when listing open PRs; cannot verify previous release PRs are merged."
95+
exit 1
96+
fi
97+
98+
UNMERGED=$(echo "$OPEN_RELEASE_PRS" | jq -r --arg cur "release/$NEW_VERSION" '
99+
[ .[]
100+
| select(.headRefName | test("^release/[0-9]+[.][0-9]+[.][0-9]+$"))
101+
| select(.headRefName != $cur) ]
102+
| .[] | " - #\(.number) \(.title) (\(.headRefName)) — \(.url)"')
103+
104+
if [ -n "$UNMERGED" ]; then
105+
echo "::error::Cannot prepare release $NEW_VERSION — a previous release PR is still open and must be merged first:"
106+
echo "$UNMERGED"
107+
exit 1
108+
fi
109+
echo "✅ No unmerged previous release PRs found."
110+
66111
- name: Get latest release tag
67112
id: get_release
68-
uses: actions/github-script@v8
69-
with:
70-
script: |
71-
try {
72-
const latestRelease = await github.rest.repos.getLatestRelease({
73-
owner: context.repo.owner,
74-
repo: context.repo.repo,
75-
});
76-
core.setOutput("latest_tag", latestRelease.data.tag_name);
77-
core.setOutput("has_release", "true");
78-
console.log("Latest release tag:", latestRelease.data.tag_name);
79-
} catch (error) {
80-
if (error.status === 404) {
81-
console.log("No previous release found. Will include all commits.");
82-
core.setOutput("has_release", "false");
83-
} else {
84-
throw error;
85-
}
86-
}
113+
env:
114+
GH_TOKEN: ${{ github.token }}
115+
run: |
116+
# Cutoff for the changelog is the COMMIT timestamp of the latest
117+
# release tag (not the release publish time), so PRs are compared
118+
# against when the release was actually cut.
119+
if TAG=$(gh release view --json tagName --jq '.tagName' 2>/dev/null); then
120+
git fetch origin tag "$TAG" --quiet || true
121+
CUTOFF=$(git log -1 --format=%ct "$TAG")
122+
echo "Latest release tag: $TAG (commit timestamp $CUTOFF)"
123+
echo "has_release=true" >> "$GITHUB_OUTPUT"
124+
echo "cutoff_timestamp=$CUTOFF" >> "$GITHUB_OUTPUT"
125+
else
126+
echo "No previous release found. Including all PRs."
127+
echo "has_release=false" >> "$GITHUB_OUTPUT"
128+
echo "cutoff_timestamp=0" >> "$GITHUB_OUTPUT"
129+
fi
87130
88131
- name: Build changelog from PRs
89132
id: changelog
90-
uses: actions/github-script@v8
91133
env:
92-
HAS_RELEASE: ${{ steps.get_release.outputs.has_release }}
93-
LATEST_TAG: ${{ steps.get_release.outputs.latest_tag }}
134+
GH_TOKEN: ${{ github.token }}
135+
RELEASE_CUTOFF_TIMESTAMP: ${{ steps.get_release.outputs.cutoff_timestamp }}
94136
NEW_VERSION: ${{ steps.set_version.outputs.new_version }}
95-
with:
96-
script: |
97-
const newVersion = process.env.NEW_VERSION.trim();
98-
const hasRelease = process.env.HAS_RELEASE === 'true';
99-
const latestTag = process.env.LATEST_TAG;
100-
101-
// Determine the cutoff date for merged PRs
102-
let since = null;
103-
if (hasRelease) {
104-
const release = await github.rest.repos.getReleaseByTag({
105-
owner: context.repo.owner,
106-
repo: context.repo.repo,
107-
tag: latestTag,
108-
});
109-
since = new Date(release.data.published_at);
110-
console.log(`Including PRs merged after ${since.toISOString()}`);
111-
} else {
112-
console.log("No previous release. Including all merged PRs.");
113-
}
114-
115-
// Query merged PRs directly — only returns PRs on this repo
116-
const prs = await github.paginate(github.rest.pulls.list, {
117-
owner: context.repo.owner,
118-
repo: context.repo.repo,
119-
state: 'closed',
120-
sort: 'updated',
121-
direction: 'desc',
122-
base: 'main',
123-
per_page: 100,
124-
});
125-
126-
const changelog = [];
127-
for (const pr of prs) {
128-
if (!pr.merged_at) continue;
129-
if (since && new Date(pr.merged_at) < since) continue;
137+
run: |
138+
# This repo receives commits via fbshipit, which CLOSES PRs instead of
139+
# merging them: mergedAt is null and a "Merged" label is applied by
140+
# tooling. Treat a PR as landed if it was merged OR carries the "Merged"
141+
# label, and use closedAt as the date fallback. Include every changelog:*
142+
# label except changelog:none; skip "Release X.Y.Z" bookkeeping PRs.
143+
CHANGELOG=""
144+
while IFS=$'\t' read -r DATE LABEL PR_TITLE PR_AUTHOR PR_NUM; do
145+
[ -z "$PR_NUM" ] && continue
146+
# Capitalize the changelog label for display.
147+
LABEL="$(echo "${LABEL:0:1}" | tr '[:lower:]' '[:upper:]')${LABEL:1}"
148+
CHANGELOG="${CHANGELOG}* ${LABEL} - ${PR_TITLE} by @${PR_AUTHOR} in #${PR_NUM}\n"
149+
done < <(
150+
gh pr list --state closed --base main --limit 1000 \
151+
--json number,title,author,labels,mergedAt,closedAt --jq '
152+
.[]
153+
| . as $pr
154+
| ($pr.mergedAt // $pr.closedAt) as $date
155+
| ([.labels[].name | ascii_downcase] | index("merged")) as $hasMergedLabel
156+
| (any(.labels[].name; ascii_downcase | test("^changelog:[[:space:]]*none$"))) as $hasChangelogNone
157+
| select(($pr.mergedAt != null) or $hasMergedLabel)
158+
| select($hasChangelogNone | not)
159+
| select($date != null)
160+
| select(($date | fromdateiso8601) > (env.RELEASE_CUTOFF_TIMESTAMP | tonumber))
161+
| ([.labels[].name | ascii_downcase | select(startswith("changelog:")) | sub("^changelog:[[:space:]]*"; "")][0] // "") as $label
162+
| select($label != "")
163+
| select((.title | test("^release\\s+[0-9]+\\.[0-9]+\\.[0-9]+"; "i")) | not)
164+
| [$date, $label, .title, .author.login, (.number | tostring)]
165+
| @tsv
166+
' | sort
167+
)
130168
131-
const labelPrefix = "changelog:";
132-
const labels = pr.labels
133-
.map(l => l.name)
134-
.filter(l => l.startsWith(labelPrefix))
135-
.map(l => l.replace(labelPrefix, "").trim());
136-
if (labels.length === 0 || labels[0].toLowerCase() === 'none') continue;
169+
# Fail loudly rather than cutting a release with an empty changelog
170+
# (e.g. if PR labels are missing or the landed-PR detection regresses).
171+
if [ -z "$CHANGELOG" ]; then
172+
echo "::error::No changelog entries were generated for $NEW_VERSION. Ensure landed PRs since the last release carry a changelog:* label (other than none)."
173+
exit 1
174+
fi
137175
138-
const category = labels[0];
139-
changelog.push(`* ${category.charAt(0).toUpperCase()}${category.slice(1)} - ${pr.title} by @${pr.user.login} in #${pr.number}`);
140-
}
176+
TODAY=$(date -u +%Y-%m-%d)
177+
OUTPUT="= ${NEW_VERSION} - ${TODAY} =\n${CHANGELOG}"
178+
echo -e "$OUTPUT"
141179
142-
const date = new Date().toISOString().slice(0, 10);
143-
const output = `= ${newVersion} - ${date} =\n${changelog.join('\n')}\n`;
144-
core.setOutput('changelog', output);
145-
console.log(output);
180+
# Write multiline output for downstream steps.
181+
{
182+
echo "changelog<<EOF"
183+
echo -e "$OUTPUT"
184+
echo "EOF"
185+
} >> "$GITHUB_OUTPUT"
146186
147187
- name: Configure Git
148188
run: |
@@ -199,6 +239,11 @@ jobs:
199239
const fs = require('fs');
200240
let content = fs.readFileSync('changelog.txt').toString().split('\n');
201241
const newLines = process.env.CHANGELOG_TEXT.split(/\r?\n/);
242+
// Normalize to exactly one blank line between this block and the next.
243+
while (newLines.length && newLines[newLines.length - 1].trim() === '') {
244+
newLines.pop();
245+
}
246+
newLines.push('');
202247
// Insert new entry after the header line
203248
content.splice(1, 0, ...newLines);
204249
fs.writeFileSync('changelog.txt', content.join('\n'));
@@ -241,9 +286,13 @@ jobs:
241286
}
242287
}
243288
244-
// Insert new changelog
289+
// Insert new changelog with exactly one blank line on each side.
245290
const newLines = process.env.CHANGELOG_TEXT.split(/\r?\n/);
246-
newLines.unshift("");
291+
while (newLines.length && newLines[newLines.length - 1].trim() === '') {
292+
newLines.pop();
293+
}
294+
newLines.unshift(""); // blank line after the "== Changelog ==" marker
295+
newLines.push(""); // single blank line before the following content
247296
content.splice(i, 0, ...newLines);
248297
fs.writeFileSync('readme.txt', content.join('\n'));
249298
@@ -254,3 +303,24 @@ jobs:
254303
git add .
255304
git commit -m "Prepare release $NEW_VERSION"
256305
git push origin HEAD
306+
307+
- name: Trigger build for the new release branch
308+
env:
309+
GH_TOKEN: ${{ github.token }}
310+
NEW_VERSION: ${{ steps.set_version.outputs.new_version }}
311+
run: |
312+
# The branch-creation push above uses GITHUB_TOKEN, which by design does
313+
# NOT trigger build-and-upload.yml's push event. Dispatch it explicitly
314+
# (workflow_dispatch is the exception to that rule) so a standalone
315+
# build-and-upload run attributed to release/$NEW_VERSION exists for
316+
# Set Stable Tag to compare the marketplace against later.
317+
gh workflow run build-and-upload.yml \
318+
--repo "${{ github.repository }}" \
319+
--ref "release/$NEW_VERSION"
320+
321+
# Also build inline as part of this run for immediate artifact availability.
322+
build-and-upload:
323+
needs: prepare-release
324+
uses: ./.github/workflows/build-and-upload.yml
325+
with:
326+
ref: refs/heads/release/${{ needs.prepare-release.outputs.version }}

.github/workflows/release-plugin.yml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,32 @@ jobs:
7070
run: |
7171
readme_file="readme.txt"
7272
found_changelog=0
73+
in_version=0
7374
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
7475
while IFS= read -r line || [[ -n $line ]]; do
7576
clean_line=$(echo "$line" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
7677
if [[ $found_changelog -eq 0 ]]; then
7778
if [[ "$clean_line" == "== Changelog ==" ]]; then
7879
found_changelog=1
7980
fi
80-
else
81-
if [[ "$clean_line" =~ ^=\ [0-9]+\.[0-9] ]]; then
81+
continue
82+
fi
83+
# Stop at the next top-level section, e.g. "== Upgrade Notice ==".
84+
if [[ "$clean_line" =~ ^==\ ]]; then
85+
break
86+
fi
87+
if [[ "$clean_line" =~ ^=\ [0-9]+\.[0-9] ]]; then
88+
# The first version header after the marker is the release we
89+
# are cutting; collect its bullets. A second version header
90+
# means we've reached the previous release, so stop.
91+
if [[ $in_version -eq 1 ]]; then
8292
break
8393
fi
84-
if [[ "$clean_line" == \** ]]; then
85-
echo "$clean_line" >> $GITHUB_ENV
86-
fi
94+
in_version=1
95+
continue
96+
fi
97+
if [[ $in_version -eq 1 && "$clean_line" == \** ]]; then
98+
echo "$clean_line" >> $GITHUB_ENV
8799
fi
88100
done < "$readme_file"
89101
echo "EOF" >> $GITHUB_ENV

0 commit comments

Comments
 (0)