1080 lines
43 KiB
YAML
1080 lines
43 KiB
YAML
name: Publish Release to Maven Central
|
|
|
|
concurrency:
|
|
group: publish-release-${{ github.repository }}
|
|
|
|
on:
|
|
pull_request:
|
|
types: [closed]
|
|
workflow_dispatch:
|
|
inputs:
|
|
releaseVersion:
|
|
description: "Release version to publish (e.g. 0.22.2)."
|
|
required: true
|
|
type: string
|
|
releaseCommit:
|
|
description: "Optional: Release commit SHA on the default branch (auto-detects from release/<version>.md if blank)."
|
|
required: false
|
|
type: string
|
|
dryRun:
|
|
description: "Run publish workflow in dry-run mode (no tag/push/deploy)."
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
recoveryMode:
|
|
description: "Require release-recovery environment approval before allowing an explicit releaseCommit that is not reachable from the default branch."
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
permissions:
|
|
actions: write
|
|
contents: write
|
|
discussions: write
|
|
|
|
jobs:
|
|
recovery_approval:
|
|
name: Release recovery approval
|
|
runs-on: ubuntu-latest
|
|
if: ${{ github.event_name == 'workflow_dispatch' && inputs.recoveryMode == true }}
|
|
environment:
|
|
name: release-recovery
|
|
steps:
|
|
- name: Approve release recovery mode
|
|
run: |
|
|
echo "Recovery mode was explicitly requested for publish-release.yml."
|
|
echo "Environment approval gates manual publishing of a release commit that may not be reachable from the default branch."
|
|
|
|
publish:
|
|
name: Publish Release to Maven Central
|
|
runs-on: ubuntu-latest
|
|
needs: recovery_approval
|
|
if: |
|
|
always() &&
|
|
(
|
|
github.event_name == 'workflow_dispatch' ||
|
|
(github.event.pull_request.merged == true &&
|
|
contains(github.event.pull_request.labels.*.name, 'release'))
|
|
) &&
|
|
(needs.recovery_approval.result == 'success' || needs.recovery_approval.result == 'skipped')
|
|
|
|
steps:
|
|
# -----------------------
|
|
# Setup and Metadata
|
|
# -----------------------
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: true
|
|
token: ${{ secrets.GH_TA4J_REPO_TOKEN || github.token }}
|
|
|
|
- name: Normalize dry run input
|
|
id: dry_run
|
|
run: |
|
|
echo "::group::Normalize publish-release inputs"
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
dry_run=false
|
|
else
|
|
raw="${{ github.event.inputs.dryRun }}"
|
|
if [ -z "$raw" ]; then
|
|
raw=true
|
|
fi
|
|
normalized=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')
|
|
|
|
if [ "$normalized" = "false" ] || [ "$normalized" = "0" ] || [ "$normalized" = "no" ]; then
|
|
dry_run=false
|
|
else
|
|
dry_run=true
|
|
fi
|
|
fi
|
|
|
|
echo "audit:dry_run_normalized=$dry_run"
|
|
echo "dryRun=$dry_run" >> $GITHUB_OUTPUT
|
|
echo "audit:recovery_mode=${{ github.event.inputs.recoveryMode }}"
|
|
echo "::endgroup::"
|
|
|
|
- name: Preflight GH_TA4J_REPO_TOKEN permissions
|
|
id: token_preflight
|
|
env:
|
|
GH_TA4J_REPO_TOKEN: ${{ secrets.GH_TA4J_REPO_TOKEN }}
|
|
DRY_RUN: ${{ steps.dry_run.outputs.dryRun }}
|
|
run: |
|
|
set -euo pipefail
|
|
python3 - <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
|
|
token = os.environ.get("GH_TA4J_REPO_TOKEN", "")
|
|
dry_run = os.environ.get("DRY_RUN", "false").lower() == "true"
|
|
|
|
if not token:
|
|
print("audit:repo_token_present=false")
|
|
sys.exit(0)
|
|
|
|
owner = os.environ.get("GITHUB_REPOSITORY_OWNER")
|
|
repo = os.environ.get("GITHUB_REPOSITORY", "").split("/", 1)[-1]
|
|
if not owner or not repo:
|
|
print("::error::Unable to resolve repository context for permission preflight.")
|
|
sys.exit(1)
|
|
|
|
query = {
|
|
"query": "query($owner:String!,$repo:String!){repository(owner:$owner,name:$repo){viewerPermission}}",
|
|
"variables": {"owner": owner, "repo": repo},
|
|
}
|
|
req = urllib.request.Request(
|
|
"https://api.github.com/graphql",
|
|
data=json.dumps(query).encode("utf-8"),
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "ta4j-release-preflight",
|
|
},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req) as resp:
|
|
data = json.load(resp)
|
|
except Exception as exc:
|
|
msg = f"GH_TA4J_REPO_TOKEN permission check failed: {exc}"
|
|
if dry_run:
|
|
print(f"::warning::{msg}")
|
|
sys.exit(0)
|
|
print(f"::error::{msg}")
|
|
sys.exit(1)
|
|
|
|
if "errors" in data:
|
|
msg = f"GH_TA4J_REPO_TOKEN permission check failed: {data['errors']}"
|
|
if dry_run:
|
|
print(f"::warning::{msg}")
|
|
sys.exit(0)
|
|
print(f"::error::{msg}")
|
|
sys.exit(1)
|
|
|
|
permission = data.get("data", {}).get("repository", {}).get("viewerPermission")
|
|
allowed = permission in ("WRITE", "MAINTAIN", "ADMIN")
|
|
|
|
output_path = os.environ.get("GITHUB_OUTPUT")
|
|
if output_path:
|
|
with open(output_path, "a", encoding="utf-8") as fh:
|
|
fh.write("repo_token_present=true\n")
|
|
fh.write(f"repo_token_permission={permission}\n")
|
|
|
|
if not allowed:
|
|
msg = f"GH_TA4J_REPO_TOKEN permission '{permission}' lacks write access."
|
|
if dry_run:
|
|
print(f"::warning::{msg}")
|
|
sys.exit(0)
|
|
print(f"::error::{msg}")
|
|
sys.exit(1)
|
|
|
|
print(f"audit:repo_token_permission={permission}")
|
|
PY
|
|
|
|
- name: Extract release metadata
|
|
id: meta
|
|
uses: actions/github-script@v9
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
const { execSync } = require("child_process");
|
|
|
|
const eventName = context.eventName;
|
|
const defaultBranch = context.payload.repository?.default_branch || "";
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
let releaseVersion = "";
|
|
let releaseCommit = "";
|
|
let nextVersion = "";
|
|
let mergeCommit = "";
|
|
let prNumber = "";
|
|
let releaseCommitInPr = "false";
|
|
|
|
const normalizeSha = (candidate) => {
|
|
const value = typeof candidate === "string" ? candidate.trim() : "";
|
|
return /^[0-9a-f]{7,40}$/i.test(value) ? value : "";
|
|
};
|
|
|
|
const fetchDefaultBranch = () => {
|
|
if (!defaultBranch) {
|
|
throw new Error("Default branch not available; cannot resolve release metadata.");
|
|
}
|
|
execSync(`git fetch origin ${defaultBranch} --tags`, { stdio: "inherit" });
|
|
};
|
|
|
|
const fetchPullRequestHead = (number) => {
|
|
execSync(
|
|
`git fetch origin pull/${number}/head:refs/remotes/origin/pull/${number}/head`,
|
|
{ stdio: "inherit" }
|
|
);
|
|
};
|
|
|
|
const fetchCommitBySha = (sha) => {
|
|
try {
|
|
execSync(`git fetch origin ${sha}`, { stdio: "ignore" });
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const detectReleaseCommitFromDefaultBranch = (version) => {
|
|
const releaseNotesPath = `release/${version}.md`;
|
|
let output = "";
|
|
try {
|
|
output = execSync(
|
|
`git log --diff-filter=A --format=%H origin/${defaultBranch} -- ${releaseNotesPath}`,
|
|
{ encoding: "utf8" }
|
|
).trim();
|
|
} catch (error) {
|
|
throw new Error(`Failed to search for release commit on origin/${defaultBranch}: ${error.message}`);
|
|
}
|
|
if (!output) {
|
|
throw new Error(
|
|
`Unable to auto-detect release commit. '${releaseNotesPath}' not found on origin/${defaultBranch}.`
|
|
);
|
|
}
|
|
return output.split(/\r?\n/)[0].trim();
|
|
};
|
|
|
|
const commitExists = (sha) => {
|
|
try {
|
|
execSync(`git cat-file -e ${sha}^{commit}`, { stdio: "ignore" });
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
if (eventName === "workflow_dispatch") {
|
|
const dispatchInputs = context.payload.inputs || {};
|
|
const readDispatchInput = (key) => {
|
|
const candidateKeys = [key, key.toLowerCase()];
|
|
for (const candidate of candidateKeys) {
|
|
const value = dispatchInputs[candidate];
|
|
if (typeof value === "string" && value.trim()) {
|
|
return value.trim();
|
|
}
|
|
}
|
|
|
|
const fromCore = core.getInput(key, { required: false });
|
|
return typeof fromCore === "string" ? fromCore.trim() : "";
|
|
};
|
|
|
|
releaseVersion = readDispatchInput("releaseVersion");
|
|
releaseCommit = normalizeSha(readDispatchInput("releaseCommit"));
|
|
if (!releaseVersion) {
|
|
const providedKeys = Object.keys(dispatchInputs);
|
|
throw new Error(
|
|
`releaseVersion is required for workflow_dispatch. Provided inputs: ${
|
|
providedKeys.length > 0 ? providedKeys.join(", ") : "(none)"
|
|
}`
|
|
);
|
|
}
|
|
} else {
|
|
const pr = context.payload.pull_request;
|
|
prNumber = pr?.number ? String(pr.number) : "";
|
|
mergeCommit = pr?.merge_commit_sha || "";
|
|
const body = pr?.body || "";
|
|
const match = body.match(/<!--\s*release-meta([\s\S]*?)-->/i);
|
|
if (!match) {
|
|
throw new Error("Release metadata block not found in PR body.");
|
|
}
|
|
const metaText = match[1];
|
|
const valueFor = (key) => {
|
|
const re = new RegExp(`^\\s*${key}\\s*:\\s*(.+)\\s*$`, "mi");
|
|
const found = metaText.match(re);
|
|
return found ? found[1].trim() : "";
|
|
};
|
|
releaseVersion = valueFor("releaseVersion");
|
|
nextVersion = valueFor("nextVersion");
|
|
releaseCommit = normalizeSha(valueFor("releaseCommit"));
|
|
|
|
if (!prNumber) {
|
|
throw new Error("Release PR number missing from event payload.");
|
|
}
|
|
const prCommitList = await github.paginate(github.rest.pulls.listCommits, {
|
|
owner,
|
|
repo,
|
|
pull_number: Number(prNumber),
|
|
per_page: 100
|
|
});
|
|
if (!prCommitList.length) {
|
|
throw new Error(`Release PR #${prNumber} has no commits.`);
|
|
}
|
|
const releaseHeadline = `Release ${releaseVersion}`;
|
|
const releaseCommitByHeadline = prCommitList.find((entry) => {
|
|
const headline = String(entry?.commit?.message || "").split(/\r?\n/, 1)[0].trim();
|
|
return headline === releaseHeadline;
|
|
});
|
|
const prCommitShas = new Set(prCommitList.map((entry) => entry.sha));
|
|
|
|
if (releaseCommit) {
|
|
if (!prCommitShas.has(releaseCommit)) {
|
|
throw new Error(
|
|
`releaseCommit ${releaseCommit} from PR metadata is not part of PR #${prNumber}.`
|
|
);
|
|
}
|
|
} else if (releaseCommitByHeadline?.sha) {
|
|
releaseCommit = releaseCommitByHeadline.sha;
|
|
core.info(
|
|
`Inferred release commit ${releaseCommit} from PR commit headline '${releaseHeadline}'.`
|
|
);
|
|
} else {
|
|
throw new Error(
|
|
`Missing releaseCommit metadata and no '${releaseHeadline}' commit found in PR #${prNumber}.`
|
|
);
|
|
}
|
|
releaseCommitInPr = "true";
|
|
}
|
|
|
|
if (!releaseVersion) {
|
|
throw new Error("Missing releaseVersion in metadata.");
|
|
}
|
|
|
|
fetchDefaultBranch();
|
|
if (eventName === "pull_request") {
|
|
fetchPullRequestHead(prNumber);
|
|
}
|
|
|
|
if (!releaseCommit) {
|
|
releaseCommit = detectReleaseCommitFromDefaultBranch(releaseVersion);
|
|
core.info(
|
|
`Using auto-detected release commit ${releaseCommit} from origin/${defaultBranch}.`
|
|
);
|
|
} else if (!commitExists(releaseCommit)) {
|
|
const fetched = fetchCommitBySha(releaseCommit);
|
|
if (!fetched || !commitExists(releaseCommit)) {
|
|
throw new Error(`Release commit ${releaseCommit} could not be fetched.`);
|
|
}
|
|
core.info(`Fetched release commit ${releaseCommit} by SHA.`);
|
|
}
|
|
|
|
core.setOutput("release_version", releaseVersion);
|
|
core.setOutput("release_commit", releaseCommit);
|
|
core.setOutput("next_version", nextVersion);
|
|
core.setOutput("merge_commit", mergeCommit);
|
|
core.setOutput("pr_number", prNumber);
|
|
core.setOutput("release_commit_in_pr", releaseCommitInPr);
|
|
|
|
- name: Verify release commit ancestry
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
RELEASE_COMMIT: ${{ steps.meta.outputs.release_commit }}
|
|
RELEASE_COMMIT_IN_PR: ${{ steps.meta.outputs.release_commit_in_pr }}
|
|
MERGE_COMMIT: ${{ steps.meta.outputs.merge_commit }}
|
|
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
RECOVERY_MODE: ${{ github.event.inputs.recoveryMode }}
|
|
run: |
|
|
set -euo pipefail
|
|
echo "::group::Verify release commit ancestry"
|
|
|
|
if [ -z "${RELEASE_VERSION:-}" ] || [ -z "${RELEASE_COMMIT:-}" ]; then
|
|
echo "Missing release metadata; cannot proceed." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${RELEASE_VERSION}" == *-SNAPSHOT* ]]; then
|
|
echo "Release version must not include -SNAPSHOT: ${RELEASE_VERSION}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! git cat-file -e "${RELEASE_COMMIT}^{commit}" 2>/dev/null; then
|
|
echo "Release commit ${RELEASE_COMMIT} not found." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "${DEFAULT_BRANCH:-}" ]; then
|
|
echo "Default branch not provided by event." >&2
|
|
exit 1
|
|
fi
|
|
|
|
git fetch origin "${DEFAULT_BRANCH}" --tags
|
|
|
|
if [ "${EVENT_NAME}" = "pull_request" ]; then
|
|
if [ "${RELEASE_COMMIT_IN_PR:-false}" != "true" ]; then
|
|
echo "Release commit ${RELEASE_COMMIT} was not validated as part of the release PR." >&2
|
|
exit 1
|
|
fi
|
|
if [ -z "${MERGE_COMMIT:-}" ]; then
|
|
echo "Merge commit SHA missing for release PR." >&2
|
|
exit 1
|
|
fi
|
|
if ! git cat-file -e "${MERGE_COMMIT}^{commit}" 2>/dev/null; then
|
|
echo "Merge commit ${MERGE_COMMIT} not found." >&2
|
|
exit 1
|
|
fi
|
|
if ! git merge-base --is-ancestor "${MERGE_COMMIT}" "origin/${DEFAULT_BRANCH}"; then
|
|
echo "Merge commit ${MERGE_COMMIT} is not on origin/${DEFAULT_BRANCH}." >&2
|
|
exit 1
|
|
fi
|
|
parents=$(git cat-file -p "${MERGE_COMMIT}" | awk '/^parent / {count++} END {print count+0}')
|
|
if [ "${parents:-0}" -ge 2 ]; then
|
|
if ! git merge-base --is-ancestor "${RELEASE_COMMIT}" "${MERGE_COMMIT}"; then
|
|
echo "Release commit ${RELEASE_COMMIT} is not an ancestor of merge commit ${MERGE_COMMIT}." >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "Merge commit ${MERGE_COMMIT} has ${parents:-0} parent(s); accepting release commit from PR metadata."
|
|
fi
|
|
elif ! git merge-base --is-ancestor "${RELEASE_COMMIT}" "origin/${DEFAULT_BRANCH}"; then
|
|
recovery_mode=$(printf '%s' "${RECOVERY_MODE:-false}" | tr '[:upper:]' '[:lower:]')
|
|
if [ "$recovery_mode" != "true" ]; then
|
|
echo "::error::Release commit ${RELEASE_COMMIT} is not on origin/${DEFAULT_BRANCH}."
|
|
echo "::error::Re-run workflow_dispatch with recoveryMode=true to require release-recovery environment approval."
|
|
exit 1
|
|
fi
|
|
echo "::warning::Release commit ${RELEASE_COMMIT} is not on origin/${DEFAULT_BRANCH}; continuing only because workflow_dispatch recoveryMode=true passed environment approval."
|
|
else
|
|
echo "Release commit ${RELEASE_COMMIT} is on origin/${DEFAULT_BRANCH}."
|
|
fi
|
|
echo "::endgroup::"
|
|
|
|
- name: Check if tag already exists
|
|
id: check_tag
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
run: |
|
|
exists=false
|
|
found=""
|
|
|
|
for candidate in "$RELEASE_VERSION" "v$RELEASE_VERSION"; do
|
|
if git rev-parse "refs/tags/$candidate" >/dev/null 2>&1; then
|
|
exists=true
|
|
found="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
|
|
echo "exists=$exists" >> $GITHUB_OUTPUT
|
|
echo "existing_tag=$found" >> $GITHUB_OUTPUT
|
|
|
|
- name: Fail if tag exists
|
|
if: steps.check_tag.outputs.exists == 'true' && steps.dry_run.outputs.dryRun != 'true'
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
EXISTING_TAG: ${{ steps.check_tag.outputs.existing_tag }}
|
|
run: |
|
|
tag="${EXISTING_TAG:-$RELEASE_VERSION}"
|
|
echo "Error: tag '$tag' already exists (checked '$RELEASE_VERSION' and 'v$RELEASE_VERSION'). Aborting."
|
|
exit 1
|
|
|
|
- name: Report existing tag in dry-run mode
|
|
if: steps.check_tag.outputs.exists == 'true' && steps.dry_run.outputs.dryRun == 'true'
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
EXISTING_TAG: ${{ steps.check_tag.outputs.existing_tag }}
|
|
run: |
|
|
tag="${EXISTING_TAG:-$RELEASE_VERSION}"
|
|
echo "::warning::Tag '$tag' already exists (checked '$RELEASE_VERSION' and 'v$RELEASE_VERSION'); dry-run validation will continue without tag mutation."
|
|
echo "audit:existing_tag_dry_run=${tag}"
|
|
|
|
# -----------------------
|
|
# Secrets and Environment
|
|
# -----------------------
|
|
- name: Check for MAVEN_CENTRAL_TOKEN_USER
|
|
id: check_maven_user
|
|
env:
|
|
MAVEN_CENTRAL_TOKEN_USER: ${{ secrets.MAVEN_CENTRAL_TOKEN_USER }}
|
|
run: |
|
|
if [ -z "${MAVEN_CENTRAL_TOKEN_USER:-}" ]; then
|
|
echo "present=false" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "present=true" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Check for MAVEN_CENTRAL_TOKEN_PASS
|
|
id: check_maven_pass
|
|
env:
|
|
MAVEN_CENTRAL_TOKEN_PASS: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASS }}
|
|
run: |
|
|
if [ -z "${MAVEN_CENTRAL_TOKEN_PASS:-}" ]; then
|
|
echo "present=false" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "present=true" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Check for GPG_PRIVATE_KEY
|
|
id: check_gpg_key
|
|
env:
|
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
run: |
|
|
if [ -z "${GPG_PRIVATE_KEY:-}" ]; then
|
|
echo "present=false" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "present=true" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Check for GPG_PASSPHRASE
|
|
id: check_gpg_passphrase
|
|
env:
|
|
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
|
run: |
|
|
if [ -z "${GPG_PASSPHRASE:-}" ]; then
|
|
echo "present=false" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "present=true" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Collect missing secrets
|
|
id: collect_missing
|
|
env:
|
|
DRY_RUN: ${{ steps.dry_run.outputs.dryRun }}
|
|
MAVEN_USER: ${{ steps.check_maven_user.outputs.present }}
|
|
MAVEN_PASS: ${{ steps.check_maven_pass.outputs.present }}
|
|
GPG_KEY: ${{ steps.check_gpg_key.outputs.present }}
|
|
GPG_PASS: ${{ steps.check_gpg_passphrase.outputs.present }}
|
|
run: |
|
|
missing=()
|
|
if [ "${MAVEN_USER:-false}" != "true" ]; then
|
|
missing+=("MAVEN_CENTRAL_TOKEN_USER")
|
|
fi
|
|
if [ "${MAVEN_PASS:-false}" != "true" ]; then
|
|
missing+=("MAVEN_CENTRAL_TOKEN_PASS")
|
|
fi
|
|
if [ "${GPG_KEY:-false}" != "true" ]; then
|
|
missing+=("GPG_PRIVATE_KEY")
|
|
fi
|
|
if [ "${GPG_PASS:-false}" != "true" ]; then
|
|
missing+=("GPG_PASSPHRASE")
|
|
fi
|
|
|
|
if [ "${#missing[@]}" -gt 0 ]; then
|
|
echo "missing=${missing[*]}" >> $GITHUB_OUTPUT
|
|
echo "has_missing=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "missing=" >> $GITHUB_OUTPUT
|
|
echo "has_missing=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Handle missing secrets in dry-run mode
|
|
if: steps.collect_missing.outputs.has_missing == 'true' && steps.dry_run.outputs.dryRun == 'true'
|
|
env:
|
|
MISSING: ${{ steps.collect_missing.outputs.missing }}
|
|
run: |
|
|
echo "::warning::DRY RUN MODE: Missing required secrets/resources detected"
|
|
echo "::warning::The following required resources are not configured: ${MISSING}"
|
|
echo "::warning::A real (non-dry-run) execution would fail without these resources."
|
|
echo "::warning::Please configure these secrets in the repository settings before running a real release."
|
|
echo "audit:missing_secrets_in_dry_run=${MISSING}"
|
|
|
|
- name: Fail if secrets missing in non-dry-run mode
|
|
if: steps.collect_missing.outputs.has_missing == 'true' && steps.dry_run.outputs.dryRun != 'true'
|
|
env:
|
|
MISSING: ${{ steps.collect_missing.outputs.missing }}
|
|
run: |
|
|
echo "::error::RELEASE FAILED: Missing required secrets/resources"
|
|
echo "::error::The following required resources are not configured: ${MISSING}"
|
|
echo ""
|
|
echo "::error::REQUIRED SECRETS:"
|
|
echo "::error:: 1. MAVEN_CENTRAL_TOKEN_USER - Maven Central username/token"
|
|
echo "::error:: 2. MAVEN_CENTRAL_TOKEN_PASS - Maven Central password/token"
|
|
echo "::error:: 3. GPG_PRIVATE_KEY - GPG private key for artifact signing"
|
|
echo "::error:: 4. GPG_PASSPHRASE - Passphrase for GPG private key"
|
|
echo ""
|
|
echo "::error::Please configure these secrets in:"
|
|
echo "::error:: Repository Settings -> Secrets and variables -> Actions -> New repository secret"
|
|
echo ""
|
|
exit 1
|
|
|
|
- name: All secrets verified
|
|
if: steps.collect_missing.outputs.has_missing != 'true'
|
|
run: |
|
|
echo "OK: All required secrets and resources are present"
|
|
echo "audit:all_required_secrets_present=true"
|
|
|
|
- name: Cache Maven packages
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/.m2
|
|
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-maven-
|
|
|
|
- name: Set up Java (dry-run without publishing secrets)
|
|
if: steps.dry_run.outputs.dryRun == 'true' && steps.collect_missing.outputs.has_missing == 'true'
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: temurin
|
|
java-version: 25
|
|
cache: maven
|
|
|
|
- name: Set up Java with publishing secrets
|
|
if: steps.dry_run.outputs.dryRun != 'true' || steps.collect_missing.outputs.has_missing != 'true'
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: temurin
|
|
java-version: 25
|
|
cache: maven
|
|
server-id: central
|
|
server-username: ${{ secrets.MAVEN_CENTRAL_TOKEN_USER }}
|
|
server-password: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASS }}
|
|
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
|
|
|
# -----------------------
|
|
# Deployment
|
|
# -----------------------
|
|
- name: Dry-run mode notice
|
|
if: steps.dry_run.outputs.dryRun == 'true'
|
|
run: |
|
|
echo "DRY RUN ENABLED: skipping tag/push/deploy; running validation only."
|
|
|
|
- name: Create Maven settings directory
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
run: |
|
|
mkdir -p ~/.m2
|
|
|
|
- name: Write Maven settings.xml
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
run: |
|
|
printf '%s\n' \
|
|
'<settings>' \
|
|
' <servers>' \
|
|
' <server>' \
|
|
' <id>central</id>' \
|
|
" <username>${{ secrets.MAVEN_CENTRAL_TOKEN_USER }}</username>" \
|
|
" <password>${{ secrets.MAVEN_CENTRAL_TOKEN_PASS }}</password>" \
|
|
' </server>' \
|
|
' </servers>' \
|
|
'</settings>' > ~/.m2/settings.xml
|
|
echo "settings.xml created"
|
|
|
|
- name: Check for MAVEN_MASTER_PASSPHRASE
|
|
id: check_maven_master
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
env:
|
|
MAVEN_SECURITY_MASTER: ${{ secrets.MAVEN_MASTER_PASSPHRASE }}
|
|
run: |
|
|
if [ -z "${MAVEN_SECURITY_MASTER:-}" ]; then
|
|
echo "present=false" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "present=true" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Write Maven settings-security.xml
|
|
if: steps.dry_run.outputs.dryRun != 'true' && steps.check_maven_master.outputs.present == 'true'
|
|
shell: bash
|
|
env:
|
|
MAVEN_SECURITY_MASTER: ${{ secrets.MAVEN_MASTER_PASSPHRASE }}
|
|
run: |
|
|
set -euo pipefail
|
|
printf '%s\n' \
|
|
'<?xml version="1.0" encoding="UTF-8"?>' \
|
|
'<settingsSecurity>' \
|
|
" <master>${MAVEN_SECURITY_MASTER}</master>" \
|
|
'</settingsSecurity>' > ~/.m2/settings-security.xml
|
|
chmod 600 ~/.m2/settings-security.xml
|
|
echo "settings-security.xml created"
|
|
|
|
- name: Skip Maven security settings (not configured)
|
|
if: steps.dry_run.outputs.dryRun != 'true' && steps.check_maven_master.outputs.present != 'true'
|
|
run: |
|
|
echo "MAVEN_MASTER_PASSPHRASE not set; skipping settings-security.xml generation"
|
|
|
|
- name: Configure GPG for signing
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p ~/.gnupg
|
|
chmod 700 ~/.gnupg
|
|
echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf
|
|
chmod 600 ~/.gnupg/gpg.conf
|
|
touch ~/.gnupg/gpg-agent.conf
|
|
echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
|
|
chmod 600 ~/.gnupg/gpg-agent.conf
|
|
gpgconf --kill gpg-agent 2>/dev/null || true
|
|
gpgconf --launch gpg-agent 2>/dev/null || true
|
|
echo "gpg.conf and gpg-agent.conf created"
|
|
|
|
- name: Check out release commit
|
|
env:
|
|
RELEASE_COMMIT: ${{ steps.meta.outputs.release_commit }}
|
|
run: |
|
|
echo "::group::Check out release commit"
|
|
git checkout --detach "${RELEASE_COMMIT}"
|
|
echo "::endgroup::"
|
|
|
|
- name: Validate release commit version
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
run: |
|
|
set -euo pipefail
|
|
echo "::group::Validate release commit version"
|
|
current_version="$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version | tr -d '\r')"
|
|
if [ "${current_version}" != "${RELEASE_VERSION}" ]; then
|
|
echo "Release commit version mismatch: expected ${RELEASE_VERSION}, got ${current_version}." >&2
|
|
exit 1
|
|
fi
|
|
echo "audit:release_commit_version=${current_version}"
|
|
echo "::endgroup::"
|
|
|
|
- name: Release candidate full verification
|
|
env:
|
|
QUIET_BUILD_TIMEOUT_SECONDS: 1800
|
|
RELEASE_TEST_TAG_EXCLUSIONS: integration,slow
|
|
run: |
|
|
set -euo pipefail
|
|
echo "::group::Release candidate full build gate"
|
|
echo "audit:release_test_tag_exclusions=${RELEASE_TEST_TAG_EXCLUSIONS}"
|
|
scripts/run-full-build-quiet.sh
|
|
echo "::endgroup::"
|
|
|
|
- name: Verify release candidate tree is clean
|
|
run: |
|
|
set -euo pipefail
|
|
echo "::group::Verify release candidate worktree cleanliness"
|
|
if ! git diff --exit-code -- . ':!target' >/tmp/release-candidate.diff; then
|
|
echo "::error::Release candidate verification changed tracked files. Fix formatter/license drift before publishing."
|
|
cat /tmp/release-candidate.diff
|
|
exit 1
|
|
fi
|
|
echo "audit:release_candidate_tree_clean=true"
|
|
echo "::endgroup::"
|
|
|
|
- name: Build and validate release artifact manifest
|
|
id: artifacts
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
run: |
|
|
set -euo pipefail
|
|
echo "::group::Build release artifacts for manifest validation"
|
|
mvn -B -DskipTests -Dgpg.skip=true -Pproduction-release package 2>&1 | tee release-artifact-build.log
|
|
python3 scripts/release/release_helpers.py artifact-manifest \
|
|
--version "${RELEASE_VERSION}" \
|
|
--output artifact-manifest.txt \
|
|
--github-output "$GITHUB_OUTPUT" \
|
|
--strict
|
|
echo "::endgroup::"
|
|
|
|
- name: Create release tag
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
RELEASE_COMMIT: ${{ steps.meta.outputs.release_commit }}
|
|
run: |
|
|
git config user.name "${{ github.actor }}"
|
|
git config user.email "${{ github.actor }}@users.noreply.github.com"
|
|
git tag -a "${RELEASE_VERSION}" -m "Release ${RELEASE_VERSION}" "${RELEASE_COMMIT}"
|
|
|
|
- name: Deploy Release to Maven Central
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
env:
|
|
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
|
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
|
run: |
|
|
set -euo pipefail
|
|
echo "::group::Deploy release to Maven Central"
|
|
mvn -B -Pproduction-release deploy 2>&1 | tee release-deploy.log
|
|
echo "::endgroup::"
|
|
|
|
- name: Validate Javadoc warning baseline
|
|
id: javadoc_warnings
|
|
if: always()
|
|
run: |
|
|
set -euo pipefail
|
|
echo "::group::Validate Javadoc warnings"
|
|
set +e
|
|
python3 scripts/release/release_helpers.py javadoc-warnings \
|
|
--baseline scripts/release/javadoc-warning-baseline.txt \
|
|
--output javadoc-warnings.txt \
|
|
--github-output "$GITHUB_OUTPUT" \
|
|
--fail-on-new \
|
|
release-artifact-build.log release-deploy.log
|
|
status=$?
|
|
set -e
|
|
echo "::endgroup::"
|
|
exit "$status"
|
|
|
|
- name: Push release tag
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
run: |
|
|
# Push tag only after deploy succeeds to avoid publishing a tag that doesn't match deployed artifacts.
|
|
git push origin "refs/tags/${RELEASE_VERSION}"
|
|
|
|
- name: Verify pushed tag reachability
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
RELEASE_COMMIT: ${{ steps.meta.outputs.release_commit }}
|
|
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ -z "${DEFAULT_BRANCH:-}" ]; then
|
|
echo "Default branch not provided by event." >&2
|
|
exit 1
|
|
fi
|
|
|
|
git fetch origin "${DEFAULT_BRANCH}" --tags --force
|
|
|
|
tag_commit=$(git rev-list -n 1 "refs/tags/${RELEASE_VERSION}" 2>/dev/null || true)
|
|
if [ -z "${tag_commit}" ]; then
|
|
echo "Tag ${RELEASE_VERSION} is not resolvable after push." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${tag_commit}" != "${RELEASE_COMMIT}" ]; then
|
|
echo "Tag ${RELEASE_VERSION} points to ${tag_commit}, expected ${RELEASE_COMMIT}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! git merge-base --is-ancestor "${tag_commit}" "origin/${DEFAULT_BRANCH}"; then
|
|
echo "Tag ${RELEASE_VERSION} commit ${tag_commit} is not reachable from origin/${DEFAULT_BRANCH}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Dispatch snapshot publication
|
|
if: steps.dry_run.outputs.dryRun != 'true'
|
|
uses: actions/github-script@v9
|
|
env:
|
|
RELEASE_VERSION: ${{ steps.meta.outputs.release_version }}
|
|
RELEASE_COMMIT: ${{ steps.meta.outputs.release_commit }}
|
|
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
with:
|
|
github-token: ${{ secrets.GH_TA4J_REPO_TOKEN || github.token }}
|
|
script: |
|
|
const releaseVersion = process.env.RELEASE_VERSION;
|
|
const releaseCommit = process.env.RELEASE_COMMIT;
|
|
const ref = process.env.DEFAULT_BRANCH;
|
|
if (!releaseVersion || !releaseCommit || !ref) {
|
|
throw new Error("Missing release metadata for snapshot dispatch.");
|
|
}
|
|
core.startGroup("Dispatch snapshot workflow");
|
|
core.info(`Dispatching snapshot.yml on ${ref} after release ${releaseVersion} (${releaseCommit}).`);
|
|
await github.rest.actions.createWorkflowDispatch({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
workflow_id: "snapshot.yml",
|
|
ref,
|
|
inputs: {
|
|
sourceReleaseVersion: releaseVersion,
|
|
sourceReleaseCommit: releaseCommit,
|
|
dryRun: "false"
|
|
}
|
|
});
|
|
core.endGroup();
|
|
|
|
# -----------------------
|
|
# Summary
|
|
# -----------------------
|
|
- name: Release decision summary
|
|
if: always()
|
|
env:
|
|
DRY_RUN: ${{ steps.dry_run.outputs.dryRun }}
|
|
RELEASE: ${{ steps.meta.outputs.release_version }}
|
|
NEXT: ${{ steps.meta.outputs.next_version }}
|
|
RELEASE_COMMIT: ${{ steps.meta.outputs.release_commit }}
|
|
MERGE_COMMIT: ${{ steps.meta.outputs.merge_commit }}
|
|
PR_NUMBER: ${{ steps.meta.outputs.pr_number }}
|
|
RECOVERY_MODE: ${{ github.event.inputs.recoveryMode }}
|
|
JAVADOC_WARNING_COUNT: ${{ steps.javadoc_warnings.outputs.javadoc_warning_count }}
|
|
JAVADOC_WARNING_BASELINE_COUNT: ${{ steps.javadoc_warnings.outputs.javadoc_warning_baseline_count }}
|
|
JAVADOC_WARNING_NEW_COUNT: ${{ steps.javadoc_warnings.outputs.javadoc_warning_new_count }}
|
|
run: |
|
|
echo "summary: dryRun=${DRY_RUN:-}"
|
|
echo "summary: release=${RELEASE:-}"
|
|
echo "summary: next=${NEXT:-}"
|
|
echo "summary: releaseCommit=${RELEASE_COMMIT:-}"
|
|
echo "summary: mergeCommit=${MERGE_COMMIT:-}"
|
|
echo "summary: releasePR=${PR_NUMBER:-}"
|
|
echo "summary: recoveryMode=${RECOVERY_MODE:-false}"
|
|
echo "summary: javadocWarnings=${JAVADOC_WARNING_COUNT:-unknown}"
|
|
echo "summary: javadocWarningBaseline=${JAVADOC_WARNING_BASELINE_COUNT:-unknown}"
|
|
echo "summary: newJavadocWarnings=${JAVADOC_WARNING_NEW_COUNT:-unknown}"
|
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
mutation_plan="would create the release tag, deploy to Maven Central, push the tag, dispatch snapshot.yml, and post the release discussion summary"
|
|
rerun_guidance="rerun publish-release.yml with dryRun=false after inspecting these computed values and artifacts"
|
|
else
|
|
mutation_plan="completed non-dry-run publish-release mutations"
|
|
rerun_guidance="not needed for this non-dry-run publish run"
|
|
fi
|
|
cat > release-audit.json <<EOF
|
|
{
|
|
"dryRun": "${DRY_RUN:-}",
|
|
"recoveryMode": "${RECOVERY_MODE:-false}",
|
|
"release": "${RELEASE:-}",
|
|
"next": "${NEXT:-}",
|
|
"releaseCommit": "${RELEASE_COMMIT:-}",
|
|
"mergeCommit": "${MERGE_COMMIT:-}",
|
|
"releasePR": "${PR_NUMBER:-}",
|
|
"mutationPlan": "${mutation_plan}",
|
|
"rerunGuidance": "${rerun_guidance}",
|
|
"javadocWarnings": "${JAVADOC_WARNING_COUNT:-}",
|
|
"javadocWarningBaseline": "${JAVADOC_WARNING_BASELINE_COUNT:-}",
|
|
"newJavadocWarnings": "${JAVADOC_WARNING_NEW_COUNT:-}",
|
|
"runUrl": "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
|
}
|
|
EOF
|
|
{
|
|
echo "## Publish Release"
|
|
echo ""
|
|
echo "- dry run: ${DRY_RUN:-unknown}"
|
|
echo "- recovery mode: ${RECOVERY_MODE:-false}"
|
|
echo "- release: ${RELEASE:-unknown}"
|
|
echo "- next snapshot: ${NEXT:-unknown}"
|
|
echo "- release commit: ${RELEASE_COMMIT:-unknown}"
|
|
echo "- merge commit: ${MERGE_COMMIT:-none}"
|
|
echo "- release PR: ${PR_NUMBER:-none}"
|
|
echo "- Javadoc warnings: ${JAVADOC_WARNING_COUNT:-unknown} current / ${JAVADOC_WARNING_BASELINE_COUNT:-unknown} baseline / ${JAVADOC_WARNING_NEW_COUNT:-unknown} new"
|
|
echo "- snapshot dispatch: $([ "${DRY_RUN:-false}" = "true" ] && echo "skipped in dry run" || echo "requested after tag verification")"
|
|
echo "- mutation plan: ${mutation_plan}"
|
|
echo "- rerun guidance: ${rerun_guidance}"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Upload publish-release audit artifacts
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: publish-release-audit-${{ github.run_id }}
|
|
path: |
|
|
release-audit.json
|
|
artifact-manifest.txt
|
|
release-artifact-build.log
|
|
release-deploy.log
|
|
javadoc-warnings.txt
|
|
.agents/logs/full-build-*.log
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|
|
|
|
- name: Build discussion comment
|
|
id: notify_summary
|
|
if: always()
|
|
env:
|
|
JOB_STATUS: ${{ job.status }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
DRY_RUN: ${{ steps.dry_run.outputs.dryRun }}
|
|
RELEASE: ${{ steps.meta.outputs.release_version }}
|
|
NEXT: ${{ steps.meta.outputs.next_version }}
|
|
RELEASE_COMMIT: ${{ steps.meta.outputs.release_commit }}
|
|
MERGE_COMMIT: ${{ steps.meta.outputs.merge_commit }}
|
|
PR_NUMBER: ${{ steps.meta.outputs.pr_number }}
|
|
TAG_EXISTS: ${{ steps.check_tag.outputs.exists }}
|
|
EXISTING_TAG: ${{ steps.check_tag.outputs.existing_tag }}
|
|
MISSING_SECRETS: ${{ steps.collect_missing.outputs.missing }}
|
|
HAS_MISSING: ${{ steps.collect_missing.outputs.has_missing }}
|
|
RELEASE_NOTIFY_USER: ${{ vars.RELEASE_NOTIFY_USER }}
|
|
run: |
|
|
status="${JOB_STATUS:-unknown}"
|
|
status_upper=$(printf '%s' "$status" | tr '[:lower:]' '[:upper:]')
|
|
run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
|
|
|
dry_run="${DRY_RUN:-unknown}"
|
|
release="${RELEASE:-unknown}"
|
|
next="${NEXT:-unknown}"
|
|
release_commit="${RELEASE_COMMIT:-unknown}"
|
|
merge_commit="${MERGE_COMMIT:-}"
|
|
pr_number="${PR_NUMBER:-}"
|
|
tag_exists="${TAG_EXISTS:-unknown}"
|
|
existing_tag="${EXISTING_TAG:-}"
|
|
missing_secrets="${MISSING_SECRETS:-}"
|
|
has_missing="${HAS_MISSING:-unknown}"
|
|
|
|
if [ -z "$merge_commit" ]; then
|
|
merge_commit="(none)"
|
|
fi
|
|
if [ -z "$pr_number" ]; then
|
|
pr_number="(none)"
|
|
fi
|
|
if [ -z "$existing_tag" ]; then
|
|
existing_tag="(none)"
|
|
fi
|
|
if [ -z "$missing_secrets" ]; then
|
|
missing_secrets="(none)"
|
|
fi
|
|
|
|
notify_user="${RELEASE_NOTIFY_USER:-TheCookieLab}"
|
|
|
|
if [ "$dry_run" = "true" ]; then
|
|
mode_label="dry-run"
|
|
marker_run="dry-run"
|
|
else
|
|
mode_label="production"
|
|
marker_run="real"
|
|
fi
|
|
|
|
if [ "$status" = "success" ]; then
|
|
completion_text="completed successfully"
|
|
else
|
|
completion_text="completed with failures"
|
|
fi
|
|
|
|
timestamp=$(date '+%-I:%M:%S %p on %-m.%-d.%Y')
|
|
|
|
cat > notification-body.txt <<EOF
|
|
<!-- ta4j:post-type=publish-release;run=${marker_run} -->
|
|
@${notify_user}
|
|
|
|
**Maven Central Release ${mode_label} ${completion_text} at ${timestamp}**
|
|
- Repository: ${GITHUB_REPOSITORY}
|
|
- Run: ${run_url}
|
|
- Event: ${EVENT_NAME}
|
|
- Status: ${status_upper}
|
|
|
|
Release Summary:
|
|
<details>
|
|
<summary>Show details</summary>
|
|
|
|
- dryRun: ${dry_run}
|
|
- release version: ${release}
|
|
- next snapshot: ${next}
|
|
- release commit: ${release_commit}
|
|
- merge commit: ${merge_commit}
|
|
- release PR: ${pr_number}
|
|
- tag exists: ${tag_exists}
|
|
- existing tag: ${existing_tag}
|
|
- missing secrets: ${missing_secrets}
|
|
- has missing secrets: ${has_missing}
|
|
|
|
</details>
|
|
EOF
|
|
|
|
- name: Post to Maven Central Releases discussion
|
|
if: always() && steps.dry_run.outputs.dryRun != 'true'
|
|
uses: actions/github-script@v9
|
|
env:
|
|
RELEASE_DISCUSSION_NUMBER: ${{ vars.RELEASE_DISCUSSION_NUMBER }}
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
const fs = require("fs");
|
|
const body = fs.readFileSync("notification-body.txt", "utf8");
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const rawNumber = process.env.RELEASE_DISCUSSION_NUMBER;
|
|
const number = rawNumber ? Number(rawNumber) : 1415;
|
|
if (!Number.isFinite(number)) {
|
|
throw new Error(`Invalid Release discussion number: ${rawNumber}`);
|
|
}
|
|
|
|
const query = `
|
|
query($owner: String!, $repo: String!, $number: Int!) {
|
|
repository(owner: $owner, name: $repo) {
|
|
discussion(number: $number) {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
const queryResult = await github.graphql(query, { owner, repo, number });
|
|
const discussionId = queryResult?.repository?.discussion?.id;
|
|
if (!discussionId) {
|
|
throw new Error(`Discussion ${number} not found in ${owner}/${repo}`);
|
|
}
|
|
|
|
const mutation = `
|
|
mutation($discussionId: ID!, $body: String!) {
|
|
addDiscussionComment(input: { discussionId: $discussionId, body: $body }) {
|
|
comment {
|
|
url
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
await github.graphql(mutation, { discussionId, body });
|