Skip to content

Merge pull request #50 from cometapi-dev/agent/docs-0-1-3-release-evi… #48

Merge pull request #50 from cometapi-dev/agent/docs-0-1-3-release-evi…

Merge pull request #50 from cometapi-dev/agent/docs-0-1-3-release-evi… #48

name: Release Please
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: release-please-main
cancel-in-progress: false
jobs:
release-please:
name: Prepare a reviewed release pull request or GitHub release
if: vars.RELEASE_PLEASE_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: write
issues: write
pull-requests: write
steps:
- name: Check out the current main branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.sha }}
- name: Require the exact current main commit
env:
EVENT_NAME: ${{ github.event_name }}
EXPECTED_SHA: ${{ github.sha }}
RUN_ATTEMPT: ${{ github.run_attempt }}
TRIGGERING_REF: ${{ github.ref }}
shell: bash
run: |
set -euo pipefail
if [[ "$TRIGGERING_REF" != "refs/heads/main" ]]; then
echo "Release Please must run from refs/heads/main." >&2
exit 1
fi
if [[ "$EVENT_NAME" == "workflow_dispatch" && "$RUN_ATTEMPT" != "1" ]]; then
echo "Release Please preparation reruns are forbidden; start a new dispatch." >&2
exit 1
fi
if [[ "$EVENT_NAME" != "workflow_dispatch" && "$EVENT_NAME" != "push" ]]; then
echo "Release Please received an unsupported event." >&2
exit 1
fi
if [[ "$(git rev-parse HEAD)" != "$EXPECTED_SHA" ]]; then
echo "The checked-out commit does not match the triggering SHA." >&2
exit 1
fi
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "$(git rev-parse refs/remotes/origin/main)" != "$EXPECTED_SHA" ]]; then
echo "main moved after this Release Please run was triggered." >&2
exit 1
fi
- name: Set up Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: npm
- name: Install validation dependencies without lifecycle scripts
run: npm ci --ignore-scripts
- name: Validate Release Please configuration before mutation
shell: bash
run: |
set -euo pipefail
node scripts/validate-release.mjs
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import { parse } from "yaml";
import {
validatePublishWorkflowContract,
validateReleasePleaseMutationConfiguration,
} from "./scripts/release-workflow-validation.mjs";
validateReleasePleaseMutationConfiguration(
JSON.parse(readFileSync("release-please-config.json", "utf8")),
);
validatePublishWorkflowContract(
parse(readFileSync(".github/workflows/publish.yml", "utf8")),
);
EOF
- name: Reject commit-level version overrides
env:
STABLE_BOUNDARY: 1752cbb57f11dc6dca8dd1b13f0f8d5e8b5fdfca
shell: bash
run: |
set -euo pipefail
if ! git merge-base --is-ancestor "$STABLE_BOUNDARY" HEAD; then
echo "The stable 0.1.0 boundary is not an ancestor of main." >&2
exit 1
fi
commits_file="$RUNNER_TEMP/release-please-commits"
git log -z --format='%B' "${STABLE_BOUNDARY}..HEAD" > "$commits_file"
COMMITS_FILE="$commits_file" node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import { validateReleasePleaseCommitMessages } from "./scripts/release-workflow-validation.mjs";
const messages = readFileSync(process.env.COMMITS_FILE, "utf8")
.split("\0")
.filter((message) => message !== "");
validateReleasePleaseCommitMessages(messages);
EOF
- name: Inspect the exact release state for a push run
id: release-state
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
RUN_ATTEMPT: ${{ github.run_attempt }}
RUN_ID: ${{ github.run_id }}
shell: bash
run: |
set -euo pipefail
run_file="$RUNNER_TEMP/release-please-run.json"
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}" > "$run_file"
run_created_at="$(RUN_FILE="$run_file" node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import { validateReleasePleaseRunMetadata } from "./scripts/release-workflow-validation.mjs";
const result = validateReleasePleaseRunMetadata(
JSON.parse(readFileSync(process.env.RUN_FILE, "utf8")),
{
releaseCommit: process.env.GITHUB_SHA,
repository: process.env.GITHUB_REPOSITORY,
runAttempt: Number(process.env.RUN_ATTEMPT),
runId: Number(process.env.RUN_ID),
},
);
process.stdout.write(result.createdAt);
EOF
)"
version="$(node --print 'require("./package.json").version')"
manifest_version="$(node --print 'require("./.release-please-manifest.json")["."]')"
before_sha="${{ github.event.before }}"
head_sha="${{ github.event.after }}"
if [[ ! "$before_sha" =~ ^[0-9a-f]{40}$ || "$before_sha" == "0000000000000000000000000000000000000000" ]]; then
echo "Release Please push runs require an exact previous main commit." >&2
exit 1
fi
if [[ "$head_sha" != "$GITHUB_SHA" ]]; then
echo "Release Please push payload does not match the triggering SHA." >&2
exit 1
fi
git fetch --no-tags origin "$before_sha"
package_changed="false"
if git diff --name-only "$before_sha" "$GITHUB_SHA" | grep -Fxq package.json; then
package_changed="true"
fi
previous_version="$(git show "$before_sha:package.json" | node -e \
'let value="";process.stdin.on("data",chunk=>value+=chunk).on("end",()=>process.stdout.write(JSON.parse(value).version))')"
tag="v${version}"
attempts_file="$RUNNER_TEMP/release-please-prior-attempts.json"
printf '[]\n' > "$attempts_file"
if (( RUN_ATTEMPT > 1 )); then
for attempt in $(seq 1 $((RUN_ATTEMPT - 1))); do
attempt_file="$RUNNER_TEMP/release-please-attempt-${attempt}.json"
gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/attempts/${attempt}/jobs?per_page=100" \
> "$attempt_file"
next_attempts="$RUNNER_TEMP/release-please-prior-attempts-next.json"
jq --argjson attempt "$attempt" --slurpfile jobs "$attempt_file" \
'. + [{attempt: $attempt, jobs: $jobs[0].jobs}]' \
"$attempts_file" > "$next_attempts"
mv "$next_attempts" "$attempts_file"
done
fi
owner="${GITHUB_REPOSITORY%%/*}"
repository_name="${GITHUB_REPOSITORY#*/}"
state_file="$RUNNER_TEMP/release-please-presence.json"
release_file="$RUNNER_TEMP/release-please-release-before.json"
# shellcheck disable=SC2016
gh api graphql \
-f owner="$owner" \
-f name="$repository_name" \
-f tag="$tag" \
-f qualifiedName="refs/tags/${tag}" \
-f query='query($owner: String!, $name: String!, $tag: String!, $qualifiedName: String!) { repository(owner: $owner, name: $name) { release(tagName: $tag) { id } ref(qualifiedName: $qualifiedName) { name } } }' \
> "$state_file"
release_exists="$(jq -r '.data.repository.release != null' "$state_file")"
tag_exists="$(jq -r '.data.repository.ref != null' "$state_file")"
tag_commit=""
if [[ "$release_exists" == "true" ]]; then
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" > "$release_file"
else
printf 'null\n' > "$release_file"
fi
if [[ "$tag_exists" == "true" ]]; then
git fetch --no-tags origin \
"+refs/tags/${tag}:refs/tags/${tag}"
tag_commit="$(git rev-parse --verify "refs/tags/${tag}^{commit}")"
fi
operation="$(CURRENT_RELEASE_FILE="$release_file" \
CURRENT_TAG_COMMIT="$tag_commit" PREVIOUS_VERSION="$previous_version" \
MANIFEST_VERSION="$manifest_version" PACKAGE_CHANGED="$package_changed" \
VERSION="$version" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import { classifyPushReleasePresence } from "./scripts/release-workflow-validation.mjs";
const result = classifyPushReleasePresence({
currentRelease: JSON.parse(
readFileSync(process.env.CURRENT_RELEASE_FILE, "utf8"),
),
currentTagCommit:
process.env.CURRENT_TAG_COMMIT === ""
? null
: process.env.CURRENT_TAG_COMMIT,
headCommit: process.env.GITHUB_SHA,
manifestVersion: process.env.MANIFEST_VERSION,
packageChanged: process.env.PACKAGE_CHANGED === "true",
previousVersion: process.env.PREVIOUS_VERSION,
version: process.env.VERSION,
});
process.stdout.write(result.mode);
EOF
)"
echo "operation=${operation}" >> "$GITHUB_OUTPUT"
if [[ "$operation" == "prepare" ]]; then
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "run-created-at=${run_created_at}" >> "$GITHUB_OUTPUT"
echo "The source push will prepare or refresh the canonical patch PR."
exit 0
fi
if [[ "$operation" != "release" ]]; then
echo "Release Please returned an unsupported push operation." >&2
exit 1
fi
ATTEMPTS_FILE="$attempts_file" RELEASE_FILE="$release_file" \
RUN_CREATED_AT="$run_created_at" RUN_ID="$RUN_ID" \
TAG_COMMIT="$tag_commit" VERSION="$version" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import {
extractReleaseNotesFromChangelog,
validateReleasePresenceBeforeAction,
} from "./scripts/release-workflow-validation.mjs";
validateReleasePresenceBeforeAction({
attempts: JSON.parse(
readFileSync(process.env.ATTEMPTS_FILE, "utf8"),
),
expectedReleaseNotes: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
process.env.VERSION,
),
release: JSON.parse(readFileSync(process.env.RELEASE_FILE, "utf8")),
releaseCommit: process.env.GITHUB_SHA,
repository: process.env.GITHUB_REPOSITORY,
runAttempt: Number(process.env.RUN_ATTEMPT),
runCreatedAt: process.env.RUN_CREATED_AT,
runId: Number(process.env.RUN_ID),
tagCommit: process.env.TAG_COMMIT === "" ? null : process.env.TAG_COMMIT,
version: process.env.VERSION,
});
EOF
echo "exists=${release_exists}" >> "$GITHUB_OUTPUT"
echo "run-created-at=${run_created_at}" >> "$GITHUB_OUTPUT"
- name: Reject an unrelated stale Release Please branch
id: branch-state
if: steps.release-state.outputs.operation != 'ignore'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: release-please--branches--main--components--cometapi
RELEASE_EXISTS: ${{ steps.release-state.outputs.exists || 'false' }}
shell: bash
run: |
set -euo pipefail
branch_exists="false"
is_ancestor="false"
branch_sha=""
branch_version=""
manifest_version=""
main_version="$(node --print 'require("./package.json").version')"
pull_requests_file="$RUNNER_TEMP/release-please-pulls.json"
remote_ref="refs/heads/${RELEASE_BRANCH}"
if git ls-remote --exit-code --heads origin "$remote_ref" >/dev/null; then
branch_exists="true"
git fetch --no-tags origin \
"+${remote_ref}:refs/remotes/origin/${RELEASE_BRANCH}"
release_ref="refs/remotes/origin/${RELEASE_BRANCH}"
branch_sha="$(git rev-parse "$release_ref")"
branch_version="$(git show "$release_ref:package.json" | node -e \
'let value="";process.stdin.on("data",chunk=>value+=chunk).on("end",()=>process.stdout.write(JSON.parse(value).version))')"
manifest_version="$(git show "$release_ref:.release-please-manifest.json" | node -e \
'let value="";process.stdin.on("data",chunk=>value+=chunk).on("end",()=>process.stdout.write(JSON.parse(value)["."]))')"
if git merge-base --is-ancestor "$release_ref" refs/remotes/origin/main; then
is_ancestor="true"
fi
fi
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls?state=all&base=main&per_page=100" \
| jq 'add' > "$pull_requests_file"
BRANCH_EXISTS="$branch_exists" BRANCH_SHA="$branch_sha" \
BRANCH_VERSION="$branch_version" IS_ANCESTOR="$is_ancestor" \
MAIN_VERSION="$main_version" MANIFEST_VERSION="$manifest_version" \
PULL_REQUESTS_FILE="$pull_requests_file" \
node --input-type=module <<'EOF'
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import {
validateOpenReleasePullRequestCollisions,
validateReleasePleaseBranchState,
} from "./scripts/release-workflow-validation.mjs";
const branchSha = process.env.BRANCH_SHA;
const rawPullRequests = JSON.parse(
readFileSync(process.env.PULL_REQUESTS_FILE, "utf8"),
);
const pullRequests = rawPullRequests.map((pullRequest) => ({
author: pullRequest.user?.login,
baseRef: pullRequest.base?.ref,
body: pullRequest.body,
headRef: pullRequest.head?.ref,
headRepository: pullRequest.head?.repo?.full_name,
headSha: pullRequest.head?.sha,
labels: pullRequest.labels?.map((label) => label.name),
mergeCommitIsAncestor:
typeof pullRequest.merge_commit_sha === "string" &&
/^[0-9a-f]{40}$/.test(pullRequest.merge_commit_sha) &&
spawnSync(
"git",
[
"merge-base",
"--is-ancestor",
pullRequest.merge_commit_sha,
"refs/remotes/origin/main",
],
{ stdio: "ignore" },
).status === 0,
mergeCommitSha: pullRequest.merge_commit_sha,
mergedAt: pullRequest.merged_at,
number: pullRequest.number,
state: pullRequest.state,
title: pullRequest.title,
}));
validateOpenReleasePullRequestCollisions(pullRequests, {
branchSha: branchSha === "" ? null : branchSha,
releaseBranch: process.env.RELEASE_BRANCH,
repository: process.env.GITHUB_REPOSITORY,
});
validateReleasePleaseBranchState({
branchSha,
branchVersion: process.env.BRANCH_VERSION,
exists: process.env.BRANCH_EXISTS === "true",
isAncestor: process.env.IS_ANCESTOR === "true",
mainVersion: process.env.MAIN_VERSION,
manifestVersion: process.env.MANIFEST_VERSION,
pullRequests: pullRequests.filter(
(pullRequest) =>
pullRequest.baseRef === "main" &&
pullRequest.headRef === process.env.RELEASE_BRANCH &&
pullRequest.headSha === branchSha,
),
releaseBranch: process.env.RELEASE_BRANCH,
repository: process.env.GITHUB_REPOSITORY,
requirePendingLabel: process.env.RELEASE_EXISTS !== "true",
});
EOF
echo "branch-exists=${branch_exists}" >> "$GITHUB_OUTPUT"
echo "branch-sha=${branch_sha}" >> "$GITHUB_OUTPUT"
- name: Classify the release operation
id: preflight
if: steps.release-state.outputs.operation != 'ignore'
env:
EVENT_NAME: ${{ github.event_name }}
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: release-please--branches--main--components--cometapi
RELEASE_EXISTS: ${{ steps.release-state.outputs.exists || 'false' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
OPERATION: ${{ steps.release-state.outputs.operation || 'prepare' }}
shell: bash
run: |
set -euo pipefail
release_pulls_file="$RUNNER_TEMP/release-pulls.json"
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls?state=closed&base=main&per_page=100" \
| jq 'add' > "$release_pulls_file"
release_pr_number="$(RELEASE_PULLS_FILE="$release_pulls_file" node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import { selectPendingReleasePullRequest } from "./scripts/release-workflow-validation.mjs";
const pulls = JSON.parse(
readFileSync(process.env.RELEASE_PULLS_FILE, "utf8"),
).map((pullRequest) => ({
author: pullRequest.user?.login,
baseRef: pullRequest.base?.ref,
body: pullRequest.body,
headRef: pullRequest.head?.ref,
headRepository: pullRequest.head?.repo?.full_name,
headSha: pullRequest.head?.sha,
labels: pullRequest.labels?.map((label) => label.name),
mergeCommitSha: pullRequest.merge_commit_sha,
mergedAt: pullRequest.merged_at,
number: pullRequest.number,
state: pullRequest.state,
title: pullRequest.title,
}));
const releasePullRequest = selectPendingReleasePullRequest(pulls, {
eventName: process.env.EVENT_NAME,
operation: process.env.OPERATION,
releaseBranch: process.env.RELEASE_BRANCH,
releaseCommit: process.env.GITHUB_SHA,
releaseExists: process.env.RELEASE_EXISTS === "true",
repository: process.env.GITHUB_REPOSITORY,
runAttempt: Number(process.env.RUN_ATTEMPT),
});
process.stdout.write(
releasePullRequest === null ? "" : String(releasePullRequest.number),
);
EOF
)"
if [[ -z "$release_pr_number" ]]; then
echo "mode=prepare" >> "$GITHUB_OUTPUT"
echo "release-pr-number=" >> "$GITHUB_OUTPUT"
echo "This first-attempt run may prepare exactly one release PR."
exit 0
fi
echo "mode=release" >> "$GITHUB_OUTPUT"
echo "release-pr-number=${release_pr_number}" >> "$GITHUB_OUTPUT"
- name: Require final release state before mutation
if: steps.preflight.outputs.mode == 'release'
shell: bash
run: |
set -euo pipefail
node scripts/validate-release.mjs \
--require-final \
--require-releasable-docs
- name: Reconfirm the branch, candidate, and review before mutation
if: steps.release-state.outputs.operation != 'ignore'
env:
EVENT_NAME: ${{ github.event_name }}
EXPECTED_BRANCH_EXISTS: ${{ steps.branch-state.outputs.branch-exists }}
EXPECTED_BRANCH_SHA: ${{ steps.branch-state.outputs.branch-sha }}
EXPECTED_MODE: ${{ steps.preflight.outputs.mode }}
EXPECTED_OPERATION: ${{ steps.release-state.outputs.operation || 'prepare' }}
EXPECTED_PR_NUMBER: ${{ steps.preflight.outputs.release-pr-number }}
EXPECTED_RELEASE_EXISTS: ${{ steps.release-state.outputs.exists || 'false' }}
EXPECTED_SHA: ${{ github.sha }}
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: release-please--branches--main--components--cometapi
RUN_ATTEMPT: ${{ github.run_attempt }}
shell: bash
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "$(git rev-parse refs/remotes/origin/main)" != "$EXPECTED_SHA" ]]; then
echo "main moved during Release Please preflight." >&2
exit 1
fi
branch_exists="false"
branch_sha=""
remote_ref="refs/heads/${RELEASE_BRANCH}"
if git ls-remote --exit-code --heads origin "$remote_ref" >/dev/null; then
branch_exists="true"
git fetch --no-tags origin \
"+${remote_ref}:refs/remotes/origin/${RELEASE_BRANCH}"
branch_sha="$(git rev-parse "refs/remotes/origin/${RELEASE_BRANCH}")"
fi
if [[ "$branch_exists" != "$EXPECTED_BRANCH_EXISTS" || "$branch_sha" != "$EXPECTED_BRANCH_SHA" ]]; then
echo "The Release Please branch changed after initial validation." >&2
exit 1
fi
release_pulls_file="$RUNNER_TEMP/release-pulls-reconfirmed.json"
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls?state=all&base=main&per_page=100" \
| jq 'add' > "$release_pulls_file"
BRANCH_SHA="$branch_sha" RELEASE_PULLS_FILE="$release_pulls_file" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import {
extractReleaseNotesFromChangelog,
selectPendingReleasePullRequest,
validateOpenReleasePullRequestCollisions,
validateReleaseCandidatePullRequest,
} from "./scripts/release-workflow-validation.mjs";
const branchSha = process.env.BRANCH_SHA;
const pulls = JSON.parse(
readFileSync(process.env.RELEASE_PULLS_FILE, "utf8"),
).map((pullRequest) => ({
author: pullRequest.user?.login,
baseRef: pullRequest.base?.ref,
body: pullRequest.body,
headRef: pullRequest.head?.ref,
headRepository: pullRequest.head?.repo?.full_name,
headSha: pullRequest.head?.sha,
labels: pullRequest.labels?.map((label) => label.name),
mergeCommitSha: pullRequest.merge_commit_sha,
mergedAt: pullRequest.merged_at,
number: pullRequest.number,
state: pullRequest.state,
title: pullRequest.title,
}));
validateOpenReleasePullRequestCollisions(pulls, {
branchSha: branchSha === "" ? null : branchSha,
releaseBranch: process.env.RELEASE_BRANCH,
repository: process.env.GITHUB_REPOSITORY,
});
const releasePullRequest = selectPendingReleasePullRequest(pulls, {
eventName: process.env.EVENT_NAME,
operation: process.env.EXPECTED_OPERATION,
releaseBranch: process.env.RELEASE_BRANCH,
releaseCommit: process.env.GITHUB_SHA,
releaseExists: process.env.EXPECTED_RELEASE_EXISTS === "true",
repository: process.env.GITHUB_REPOSITORY,
runAttempt: Number(process.env.RUN_ATTEMPT),
});
const mode = releasePullRequest === null ? "prepare" : "release";
if (mode !== process.env.EXPECTED_MODE) {
throw new Error("Release workflow candidate changed after preflight.");
}
if (releasePullRequest !== null) {
if (String(releasePullRequest.number) !== process.env.EXPECTED_PR_NUMBER) {
throw new Error("Release workflow candidate PR number changed after preflight.");
}
const version = JSON.parse(readFileSync("package.json", "utf8")).version;
validateReleaseCandidatePullRequest({
expectedReleaseNotes: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
version,
),
pullRequest: releasePullRequest,
releaseBranch: process.env.RELEASE_BRANCH,
repository: process.env.GITHUB_REPOSITORY,
requirePendingLabel: process.env.EXPECTED_RELEASE_EXISTS !== "true",
version,
});
}
EOF
cp "$release_pulls_file" \
"$RUNNER_TEMP/release-pulls-before-action.json"
if [[ "$EXPECTED_MODE" == "release" ]]; then
pull_request_file="$RUNNER_TEMP/release-pull-reconfirmed.json"
reviews_file="$RUNNER_TEMP/release-reviews-reconfirmed.json"
permissions_file="$RUNNER_TEMP/reviewer-permissions-reconfirmed.json"
gh api "repos/${GITHUB_REPOSITORY}/pulls/${EXPECTED_PR_NUMBER}" \
> "$pull_request_file"
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls/${EXPECTED_PR_NUMBER}/reviews?per_page=100" \
| jq 'add' > "$reviews_file"
printf '{}\n' > "$permissions_file"
while IFS= read -r reviewer; do
permission="$(gh api \
"repos/${GITHUB_REPOSITORY}/collaborators/${reviewer}/permission" \
--jq '.permission' 2>/dev/null || printf 'none')"
next_permissions="$RUNNER_TEMP/reviewer-permissions-reconfirmed-next.json"
jq --arg reviewer "$reviewer" --arg permission "$permission" \
'. + {($reviewer): $permission}' \
"$permissions_file" > "$next_permissions"
mv "$next_permissions" "$permissions_file"
done < <(jq -r '.[].user.login' "$reviews_file" | sort -u)
PERMISSIONS_FILE="$permissions_file" PULL_REQUEST_FILE="$pull_request_file" \
REVIEWS_FILE="$reviews_file" node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import {
extractReleaseNotesFromChangelog,
validateMergedReleasePullRequest,
} from "./scripts/release-workflow-validation.mjs";
const rawPullRequest = JSON.parse(
readFileSync(process.env.PULL_REQUEST_FILE, "utf8"),
);
const permissions = JSON.parse(
readFileSync(process.env.PERMISSIONS_FILE, "utf8"),
);
const reviews = JSON.parse(readFileSync(process.env.REVIEWS_FILE, "utf8"));
const version = JSON.parse(readFileSync("package.json", "utf8")).version;
validateMergedReleasePullRequest({
expectedReleaseNotes: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
version,
),
pullRequest: {
author: rawPullRequest.user?.login,
baseRef: rawPullRequest.base?.ref,
body: rawPullRequest.body,
headRef: rawPullRequest.head?.ref,
headRepository: rawPullRequest.head?.repo?.full_name,
headSha: rawPullRequest.head?.sha,
labels: rawPullRequest.labels?.map((label) => label.name),
mergeCommitSha: rawPullRequest.merge_commit_sha,
mergedAt: rawPullRequest.merged_at,
number: rawPullRequest.number,
state: rawPullRequest.state,
title: rawPullRequest.title,
},
releaseBranch: process.env.RELEASE_BRANCH,
releaseCommit: process.env.GITHUB_SHA,
repository: process.env.GITHUB_REPOSITORY,
requirePendingLabel: process.env.EXPECTED_RELEASE_EXISTS !== "true",
reviews: reviews.map((review) => ({
commitId: review.commit_id,
id: review.id,
login: review.user?.login,
permission: permissions[review.user?.login] ?? "none",
state: review.state,
userType: review.user?.type,
})),
version,
});
EOF
fi
- name: Reconfirm the exact release state before mutation
if: github.event_name == 'push' && steps.preflight.outputs.mode == 'release'
env:
EXPECTED_RELEASE_EXISTS: ${{ steps.release-state.outputs.exists }}
GH_TOKEN: ${{ github.token }}
RUN_ATTEMPT: ${{ github.run_attempt }}
RUN_CREATED_AT: ${{ steps.release-state.outputs.run-created-at }}
RUN_ID: ${{ github.run_id }}
shell: bash
run: |
set -euo pipefail
version="$(node --print 'require("./package.json").version')"
tag="v${version}"
attempts_file="$RUNNER_TEMP/release-please-prior-attempts.json"
owner="${GITHUB_REPOSITORY%%/*}"
repository_name="${GITHUB_REPOSITORY#*/}"
state_file="$RUNNER_TEMP/release-please-presence-reconfirmed.json"
release_file="$RUNNER_TEMP/release-please-release-reconfirmed.json"
# shellcheck disable=SC2016
gh api graphql \
-f owner="$owner" \
-f name="$repository_name" \
-f tag="$tag" \
-f qualifiedName="refs/tags/${tag}" \
-f query='query($owner: String!, $name: String!, $tag: String!, $qualifiedName: String!) { repository(owner: $owner, name: $name) { release(tagName: $tag) { id } ref(qualifiedName: $qualifiedName) { name } } }' \
> "$state_file"
release_exists="$(jq -r '.data.repository.release != null' "$state_file")"
tag_exists="$(jq -r '.data.repository.ref != null' "$state_file")"
tag_commit=""
if [[ "$release_exists" == "true" ]]; then
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" > "$release_file"
else
printf 'null\n' > "$release_file"
fi
if [[ "$tag_exists" == "true" ]]; then
git fetch --no-tags origin \
"+refs/tags/${tag}:refs/tags/${tag}"
tag_commit="$(git rev-parse --verify "refs/tags/${tag}^{commit}")"
fi
ATTEMPTS_FILE="$attempts_file" RELEASE_FILE="$release_file" \
RUN_ID="$RUN_ID" TAG_COMMIT="$tag_commit" VERSION="$version" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import {
extractReleaseNotesFromChangelog,
validateReleasePresenceBeforeAction,
} from "./scripts/release-workflow-validation.mjs";
validateReleasePresenceBeforeAction({
attempts: JSON.parse(
readFileSync(process.env.ATTEMPTS_FILE, "utf8"),
),
expectedReleaseNotes: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
process.env.VERSION,
),
release: JSON.parse(readFileSync(process.env.RELEASE_FILE, "utf8")),
releaseCommit: process.env.GITHUB_SHA,
repository: process.env.GITHUB_REPOSITORY,
runAttempt: Number(process.env.RUN_ATTEMPT),
runCreatedAt: process.env.RUN_CREATED_AT,
runId: Number(process.env.RUN_ID),
tagCommit: process.env.TAG_COMMIT === "" ? null : process.env.TAG_COMMIT,
version: process.env.VERSION,
});
EOF
if [[ "$release_exists" != "$EXPECTED_RELEASE_EXISTS" ]]; then
echo "The exact tag or Release changed after preflight." >&2
exit 1
fi
- name: Require the exact current main commit immediately before mutation
if: steps.release-state.outputs.operation != 'ignore'
env:
EXPECTED_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "$(git rev-parse HEAD)" != "$EXPECTED_SHA" ||
"$(git rev-parse refs/remotes/origin/main)" != "$EXPECTED_SHA" ]]; then
echo "main moved immediately before the Release Please mutation." >&2
exit 1
fi
- name: Run Release Please
id: release
if: steps.release-state.outputs.operation != 'ignore'
continue-on-error: ${{ steps.preflight.outputs.mode == 'release' }}
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
skip-github-release: ${{ steps.preflight.outputs.mode == 'prepare' }}
skip-github-pull-request: ${{ steps.preflight.outputs.mode == 'release' }}
- name: Validate the prepared release pull request
if: steps.preflight.outputs.mode == 'prepare'
env:
GH_TOKEN: ${{ github.token }}
PRS: ${{ steps.release.outputs.prs }}
PRS_CREATED: ${{ steps.release.outputs.prs_created }}
RELEASE_BRANCH: release-please--branches--main--components--cometapi
shell: bash
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "$(git rev-parse refs/remotes/origin/main)" != "$GITHUB_SHA" ]]; then
echo "main changed while Release Please prepared the release PR." >&2
exit 1
fi
case "$PRS_CREATED" in
true) prs_created="true" ;;
false | "") prs_created="false" ;;
*)
echo "Release Please returned an invalid prs_created output." >&2
exit 1
;;
esac
pull_requests_file="$RUNNER_TEMP/prepared-release-pulls.json"
pull_request_file="$RUNNER_TEMP/prepared-release-pull.json"
pull_files_file="$RUNNER_TEMP/prepared-release-files.json"
changelog_file="$RUNNER_TEMP/prepared-release-changelog.md"
git fetch --no-tags origin \
"+refs/heads/${RELEASE_BRANCH}:refs/remotes/origin/${RELEASE_BRANCH}"
release_ref="refs/remotes/origin/${RELEASE_BRANCH}"
branch_sha="$(git rev-parse "$release_ref")"
branch_parent="$(git rev-parse "${release_ref}^")"
if [[ "$branch_parent" != "$GITHUB_SHA" ]]; then
echo "The prepared Release Please branch is not based directly on current main." >&2
exit 1
fi
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls?state=open&base=main&per_page=100" \
| jq 'add' > "$pull_requests_file"
release_pr_number="$(BRANCH_SHA="$branch_sha" \
PULL_REQUESTS_FILE="$pull_requests_file" node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import { validateOpenReleasePullRequestCollisions } from "./scripts/release-workflow-validation.mjs";
const pulls = JSON.parse(
readFileSync(process.env.PULL_REQUESTS_FILE, "utf8"),
).map((pullRequest) => ({
baseRef: pullRequest.base?.ref,
headRef: pullRequest.head?.ref,
headRepository: pullRequest.head?.repo?.full_name,
headSha: pullRequest.head?.sha,
number: pullRequest.number,
state: pullRequest.state,
}));
validateOpenReleasePullRequestCollisions(pulls, {
branchSha: process.env.BRANCH_SHA,
releaseBranch: process.env.RELEASE_BRANCH,
repository: process.env.GITHUB_REPOSITORY,
});
const candidates = pulls.filter(
(pullRequest) =>
pullRequest.baseRef === "main" &&
pullRequest.headRef === process.env.RELEASE_BRANCH &&
pullRequest.headRepository === process.env.GITHUB_REPOSITORY &&
pullRequest.headSha === process.env.BRANCH_SHA &&
pullRequest.state === "open",
);
if (candidates.length !== 1) {
throw new Error(
"Release workflow preparation requires exactly one canonical open release PR.",
);
}
process.stdout.write(String(candidates[0].number));
EOF
)"
gh api "repos/${GITHUB_REPOSITORY}/pulls/${release_pr_number}" \
> "$pull_request_file"
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls/${release_pr_number}/files?per_page=100" \
| jq 'add' > "$pull_files_file"
branch_version="$(git show "$release_ref:package.json" | node -e \
'let value="";process.stdin.on("data",chunk=>value+=chunk).on("end",()=>process.stdout.write(JSON.parse(value).version))')"
manifest_version="$(git show "$release_ref:.release-please-manifest.json" | node -e \
'let value="";process.stdin.on("data",chunk=>value+=chunk).on("end",()=>process.stdout.write(JSON.parse(value)["."]))')"
package_lock_version="$(git show "$release_ref:package-lock.json" | node -e \
'let value="";process.stdin.on("data",chunk=>value+=chunk).on("end",()=>process.stdout.write(JSON.parse(value).version))')"
package_lock_package_version="$(git show "$release_ref:package-lock.json" | node -e \
'let value="";process.stdin.on("data",chunk=>value+=chunk).on("end",()=>process.stdout.write(JSON.parse(value).packages[""].version))')"
git show "$release_ref:CHANGELOG.md" > "$changelog_file"
BRANCH_SHA="$branch_sha" BRANCH_VERSION="$branch_version" \
CHANGELOG_FILE="$changelog_file" MANIFEST_VERSION="$manifest_version" \
PACKAGE_LOCK_PACKAGE_VERSION="$package_lock_package_version" \
PACKAGE_LOCK_VERSION="$package_lock_version" PRS_CREATED_NORMALIZED="$prs_created" \
PULL_FILES_FILE="$pull_files_file" PULL_REQUEST_FILE="$pull_request_file" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import { validatePreparedReleasePullRequest } from "./scripts/release-workflow-validation.mjs";
const rawPullRequest = JSON.parse(
readFileSync(process.env.PULL_REQUEST_FILE, "utf8"),
);
const files = JSON.parse(
readFileSync(process.env.PULL_FILES_FILE, "utf8"),
).map((file) => file.filename);
const actionPullRequests =
process.env.PRS === "" ? [] : JSON.parse(process.env.PRS);
const mainVersion = JSON.parse(readFileSync("package.json", "utf8")).version;
validatePreparedReleasePullRequest({
actionPullRequests,
actionPullRequestsCreated: process.env.PRS_CREATED_NORMALIZED === "true",
branchSha: process.env.BRANCH_SHA,
branchVersion: process.env.BRANCH_VERSION,
changelog: readFileSync(process.env.CHANGELOG_FILE, "utf8"),
mainVersion,
manifestVersion: process.env.MANIFEST_VERSION,
packageLockPackageVersion: process.env.PACKAGE_LOCK_PACKAGE_VERSION,
packageLockVersion: process.env.PACKAGE_LOCK_VERSION,
pullRequest: {
author: rawPullRequest.user?.login,
baseRef: rawPullRequest.base?.ref,
body: rawPullRequest.body,
files,
headRef: rawPullRequest.head?.ref,
headRepository: rawPullRequest.head?.repo?.full_name,
headSha: rawPullRequest.head?.sha,
labels: rawPullRequest.labels?.map((label) => label.name),
mergeCommitSha: rawPullRequest.merge_commit_sha,
mergedAt: rawPullRequest.merged_at,
number: rawPullRequest.number,
state: rawPullRequest.state,
title: rawPullRequest.title,
},
releaseBranch: process.env.RELEASE_BRANCH,
repository: process.env.GITHUB_REPOSITORY,
});
EOF
- name: Verify and record the exact release result
if: ${{ !cancelled() && steps.preflight.outputs.mode == 'release' }}
env:
ACTION_OUTCOME: ${{ steps.release.outcome }}
EXPECTED_BRANCH_EXISTS: ${{ steps.branch-state.outputs.branch-exists }}
EXPECTED_BRANCH_SHA: ${{ steps.branch-state.outputs.branch-sha }}
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: release-please--branches--main--components--cometapi
RELEASE_CREATED: ${{ steps.release.outputs.release_created }}
RELEASE_EXISTED_BEFORE_ACTION: ${{ steps.release-state.outputs.exists }}
RELEASE_HTML_URL: ${{ steps.release.outputs.html_url }}
RELEASE_PR_NUMBER: ${{ steps.preflight.outputs.release-pr-number }}
RELEASE_SHA: ${{ steps.release.outputs.sha }}
RELEASE_TAG_NAME: ${{ steps.release.outputs.tag_name }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
RELEASED_PATHS: ${{ steps.release.outputs.paths_released }}
RUN_ATTEMPT: ${{ github.run_attempt }}
RUN_CREATED_AT: ${{ steps.release-state.outputs.run-created-at }}
RUN_ID: ${{ github.run_id }}
shell: bash
run: |
set -euo pipefail
version="$(node --print 'require("./package.json").version')"
tag="v${version}"
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "$(git rev-parse refs/remotes/origin/main)" != "$GITHUB_SHA" ]]; then
echo "main changed while Release Please was running." >&2
exit 1
fi
branch_exists="false"
branch_sha=""
remote_ref="refs/heads/${RELEASE_BRANCH}"
if git ls-remote --exit-code --heads origin "$remote_ref" >/dev/null; then
branch_exists="true"
git fetch --no-tags origin \
"+${remote_ref}:refs/remotes/origin/${RELEASE_BRANCH}"
branch_sha="$(git rev-parse "refs/remotes/origin/${RELEASE_BRANCH}")"
fi
if [[ "$branch_exists" != "$EXPECTED_BRANCH_EXISTS" || "$branch_sha" != "$EXPECTED_BRANCH_SHA" ]]; then
echo "The Release Please branch changed while the action was running." >&2
exit 1
fi
pull_requests_after="$RUNNER_TEMP/release-pulls-after-action.json"
pull_request_file="$RUNNER_TEMP/release-pull-after.json"
reviews_file="$RUNNER_TEMP/release-reviews-after.json"
permissions_file="$RUNNER_TEMP/reviewer-permissions-after.json"
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls?state=all&base=main&per_page=100" \
| jq 'add' > "$pull_requests_after"
gh api "repos/${GITHUB_REPOSITORY}/pulls/${RELEASE_PR_NUMBER}" \
> "$pull_request_file"
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/pulls/${RELEASE_PR_NUMBER}/reviews?per_page=100" \
| jq 'add' > "$reviews_file"
printf '{}\n' > "$permissions_file"
while IFS= read -r reviewer; do
permission="$(gh api \
"repos/${GITHUB_REPOSITORY}/collaborators/${reviewer}/permission" \
--jq '.permission' 2>/dev/null || printf 'none')"
next_permissions="$RUNNER_TEMP/reviewer-permissions-after-next.json"
jq --arg reviewer "$reviewer" --arg permission "$permission" \
'. + {($reviewer): $permission}' \
"$permissions_file" > "$next_permissions"
mv "$next_permissions" "$permissions_file"
done < <(jq -r '.[].user.login' "$reviews_file" | sort -u)
BEFORE_PULLS_FILE="$RUNNER_TEMP/release-pulls-before-action.json" \
AFTER_PULLS_FILE="$pull_requests_after" \
PERMISSIONS_FILE="$permissions_file" \
PULL_REQUEST_FILE="$pull_request_file" \
REVIEWS_FILE="$reviews_file" VERSION="$version" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import {
extractReleaseNotesFromChangelog,
validateMergedReleasePullRequest,
validatePostActionPullRequestSnapshot,
} from "./scripts/release-workflow-validation.mjs";
const normalizePulls = (path) =>
JSON.parse(readFileSync(path, "utf8")).map((pullRequest) => ({
author: pullRequest.user?.login,
baseRef: pullRequest.base?.ref,
body: pullRequest.body,
headRef: pullRequest.head?.ref,
headRepository: pullRequest.head?.repo?.full_name,
headSha: pullRequest.head?.sha,
labels: pullRequest.labels?.map((label) => label.name),
mergeCommitSha: pullRequest.merge_commit_sha,
mergedAt: pullRequest.merged_at,
number: pullRequest.number,
state: pullRequest.state,
title: pullRequest.title,
}));
validatePostActionPullRequestSnapshot(
normalizePulls(process.env.BEFORE_PULLS_FILE),
normalizePulls(process.env.AFTER_PULLS_FILE),
{ releasePullRequestNumber: Number(process.env.RELEASE_PR_NUMBER) },
);
const rawPullRequest = JSON.parse(
readFileSync(process.env.PULL_REQUEST_FILE, "utf8"),
);
const permissions = JSON.parse(
readFileSync(process.env.PERMISSIONS_FILE, "utf8"),
);
const reviews = JSON.parse(readFileSync(process.env.REVIEWS_FILE, "utf8"));
validateMergedReleasePullRequest({
expectedReleaseNotes: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
process.env.VERSION,
),
pullRequest: {
author: rawPullRequest.user?.login,
baseRef: rawPullRequest.base?.ref,
body: rawPullRequest.body,
headRef: rawPullRequest.head?.ref,
headRepository: rawPullRequest.head?.repo?.full_name,
headSha: rawPullRequest.head?.sha,
labels: rawPullRequest.labels?.map((label) => label.name),
mergeCommitSha: rawPullRequest.merge_commit_sha,
mergedAt: rawPullRequest.merged_at,
number: rawPullRequest.number,
state: rawPullRequest.state,
title: rawPullRequest.title,
},
releaseBranch: process.env.RELEASE_BRANCH,
releaseCommit: process.env.GITHUB_SHA,
repository: process.env.GITHUB_REPOSITORY,
requirePendingLabel: false,
reviews: reviews.map((review) => ({
commitId: review.commit_id,
id: review.id,
login: review.user?.login,
permission: permissions[review.user?.login] ?? "none",
state: review.state,
userType: review.user?.type,
})),
version: process.env.VERSION,
});
EOF
release_file="$RUNNER_TEMP/release-please-release-after.json"
completion_file="$RUNNER_TEMP/release-please-completion.json"
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" > "$release_file"
git fetch --no-tags origin \
"+refs/tags/${tag}:refs/tags/${tag}"
tag_commit="$(git rev-parse --verify "refs/tags/${tag}^{commit}")"
attempts_file="$RUNNER_TEMP/release-please-attempts-through-current.json"
printf '[]\n' > "$attempts_file"
for attempt in $(seq 1 "$RUN_ATTEMPT"); do
attempt_file="$RUNNER_TEMP/release-please-completed-attempt-${attempt}.json"
attempt_ready="false"
for api_check in {1..6}; do
gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/attempts/${attempt}/jobs?per_page=100" \
> "$attempt_file"
if [[ "$attempt" != "$RUN_ATTEMPT" ]] || \
jq -e \
--arg job "Prepare a reviewed release pull request or GitHub release" \
--arg step "Run Release Please" \
'[.jobs[] | select(.name == $job) | .steps[] | select(.name == $step and .status == "completed")] | length == 1' \
"$attempt_file" >/dev/null; then
attempt_ready="true"
break
fi
if [[ "$api_check" -lt 6 ]]; then
sleep 2
fi
done
if [[ "$attempt_ready" != "true" ]]; then
echo "Actions did not expose the completed Release Please step." >&2
exit 1
fi
next_attempts="$RUNNER_TEMP/release-please-attempts-through-current-next.json"
jq --argjson attempt "$attempt" --slurpfile jobs "$attempt_file" \
'. + [{attempt: $attempt, jobs: $jobs[0].jobs}]' \
"$attempts_file" > "$next_attempts"
mv "$next_attempts" "$attempts_file"
done
ATTEMPTS_FILE="$attempts_file" COMPLETION_FILE="$completion_file" \
RELEASE_FILE="$release_file" TAG_COMMIT="$tag_commit" \
VERSION="$version" node --input-type=module <<'EOF'
import { readFileSync, writeFileSync } from "node:fs";
import {
extractReleaseNotesFromChangelog,
validateReleasePleaseCompletion,
} from "./scripts/release-workflow-validation.mjs";
const releasedPaths =
process.env.RELEASED_PATHS === ""
? []
: JSON.parse(process.env.RELEASED_PATHS);
const completion = validateReleasePleaseCompletion({
actionResult: {
htmlUrl: process.env.RELEASE_HTML_URL,
outcome: process.env.ACTION_OUTCOME,
releaseCreated: process.env.RELEASE_CREATED === "true",
releasedPaths,
sha: process.env.RELEASE_SHA,
tagName: process.env.RELEASE_TAG_NAME,
version: process.env.RELEASE_VERSION,
},
attempts: JSON.parse(
readFileSync(process.env.ATTEMPTS_FILE, "utf8"),
),
expectedReleaseNotes: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
process.env.VERSION,
),
release: JSON.parse(readFileSync(process.env.RELEASE_FILE, "utf8")),
releaseCommit: process.env.GITHUB_SHA,
releaseExistedBeforeAction:
process.env.RELEASE_EXISTED_BEFORE_ACTION === "true",
repository: process.env.GITHUB_REPOSITORY,
runAttempt: Number(process.env.RUN_ATTEMPT),
runCreatedAt: process.env.RUN_CREATED_AT,
runId: Number(process.env.RUN_ID),
tagCommit: process.env.TAG_COMMIT,
version: process.env.VERSION,
});
writeFileSync(
process.env.COMPLETION_FILE,
`${JSON.stringify(completion)}\n`,
{ mode: 0o600 },
);
EOF
if ! jq -e '.labels | map(.name) | index("autorelease: tagged") != null' \
"$pull_request_file" >/dev/null; then
gh api --method POST \
"repos/${GITHUB_REPOSITORY}/issues/${RELEASE_PR_NUMBER}/labels" \
-f 'labels[]=autorelease: tagged' >/dev/null
fi
if jq -e '.labels | map(.name) | index("autorelease: pending") != null' \
"$pull_request_file" >/dev/null; then
gh api --method DELETE \
"repos/${GITHUB_REPOSITORY}/issues/${RELEASE_PR_NUMBER}/labels/autorelease%3A%20pending" \
>/dev/null
fi
gh api "repos/${GITHUB_REPOSITORY}/pulls/${RELEASE_PR_NUMBER}" \
> "$pull_request_file"
PULL_REQUEST_FILE="$pull_request_file" VERSION="$version" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import {
extractReleaseNotesFromChangelog,
validateTaggedReleasePullRequest,
} from "./scripts/release-workflow-validation.mjs";
const pullRequest = JSON.parse(
readFileSync(process.env.PULL_REQUEST_FILE, "utf8"),
);
validateTaggedReleasePullRequest({
expectedReleaseNotes: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
process.env.VERSION,
),
pullRequest: {
author: pullRequest.user?.login,
baseRef: pullRequest.base?.ref,
body: pullRequest.body,
headRef: pullRequest.head?.ref,
headRepository: pullRequest.head?.repo?.full_name,
headSha: pullRequest.head?.sha,
labels: pullRequest.labels?.map((label) => label.name),
mergeCommitSha: pullRequest.merge_commit_sha,
mergedAt: pullRequest.merged_at,
number: pullRequest.number,
state: pullRequest.state,
title: pullRequest.title,
},
releaseBranch: process.env.RELEASE_BRANCH,
releaseCommit: process.env.GITHUB_SHA,
repository: process.env.GITHUB_REPOSITORY,
version: process.env.VERSION,
});
EOF
final_release_file="$RUNNER_TEMP/release-please-release-final.json"
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" > "$final_release_file"
git fetch --no-tags origin \
"+refs/tags/${tag}:refs/tags/${tag}"
final_tag_commit="$(git rev-parse --verify "refs/tags/${tag}^{commit}")"
tagged_publish_workflow="$RUNNER_TEMP/release-tag-publish.yml"
git show "refs/tags/${tag}:.github/workflows/publish.yml" \
> "$tagged_publish_workflow"
mkdir -p release-please-result
COMPLETION_FILE="$completion_file" FINAL_RELEASE_FILE="$final_release_file" \
FINAL_TAG_COMMIT="$final_tag_commit" \
TAGGED_PUBLISH_WORKFLOW="$tagged_publish_workflow" VERSION="$version" \
node --input-type=module <<'EOF'
import { readFileSync, writeFileSync } from "node:fs";
import { parse } from "yaml";
import {
extractReleaseNotesFromChangelog,
validateGitHubRelease,
validatePublishWorkflowContract,
validateReleasePleaseActionResult,
} from "./scripts/release-workflow-validation.mjs";
const completion = JSON.parse(
readFileSync(process.env.COMPLETION_FILE, "utf8"),
);
validateGitHubRelease(
JSON.parse(readFileSync(process.env.FINAL_RELEASE_FILE, "utf8")),
{
expectedBody: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
process.env.VERSION,
),
htmlUrl: completion.htmlUrl,
releaseCommit: process.env.GITHUB_SHA,
tag: completion.tagName,
tagCommit: process.env.FINAL_TAG_COMMIT,
},
);
validatePublishWorkflowContract(
parse(readFileSync(process.env.TAGGED_PUBLISH_WORKFLOW, "utf8")),
);
const result = {
actionOutcome: completion.actionOutcome,
htmlUrl: completion.htmlUrl,
recovered: completion.recovered,
releaseCreated: completion.releaseCreated,
releaseExistedBeforeAction: completion.releaseExistedBeforeAction,
releaseSourceAttempt: completion.releaseSourceAttempt,
repository: process.env.GITHUB_REPOSITORY,
runAttempt: Number(process.env.RUN_ATTEMPT),
runId: Number(process.env.RUN_ID),
schemaVersion: 2,
sha: completion.sha,
tagName: completion.tagName,
version: completion.version,
workflowName: "Release Please",
workflowPath: ".github/workflows/release-please.yml",
};
validateReleasePleaseActionResult(result, {
releaseCommit: process.env.GITHUB_SHA,
repository: process.env.GITHUB_REPOSITORY,
runAttempt: Number(process.env.RUN_ATTEMPT),
runId: Number(process.env.RUN_ID),
version: process.env.VERSION,
workflowName: "Release Please",
workflowPath: ".github/workflows/release-please.yml",
});
writeFileSync(
"release-please-result/result.json",
`${JSON.stringify(result)}\n`,
{ mode: 0o600 },
);
EOF
- name: Upload the exact Release Please result
if: steps.preflight.outputs.mode == 'release'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-please-result-${{ github.run_id }}-${{ github.run_attempt }}
path: release-please-result/result.json
if-no-files-found: error
retention-days: 30