Files
goldenChart/ta4j-master/.github/workflows/github-release.yml
T
2026-05-23 15:11:48 +09:00

324 lines
12 KiB
YAML

name: Create GitHub Release
on:
push:
tags:
- "*.*.*"
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish (e.g. 0.22.0)"
required: true
type: string
dryRun:
description: "Run in dry-run mode to preview release creation without publishing."
required: false
default: true
type: boolean
concurrency:
group: github-release-${{ github.repository }}
permissions:
contents: write
jobs:
create_release:
name: Create GitHub Release
runs-on: ubuntu-latest
env:
GH_TA4J_REPO_TOKEN: ${{ secrets.GH_TA4J_REPO_TOKEN }}
steps:
- name: Determine checkout ref
id: checkout_ref
run: |
INPUT_TAG="${{ github.event.inputs.tag }}"
if [[ -n "$INPUT_TAG" ]]; then
# For workflow_dispatch, construct tag ref from input
if [[ "$INPUT_TAG" == refs/tags/* ]]; then
INPUT_TAG="${INPUT_TAG#refs/tags/}"
fi
CHECKOUT_REF="refs/tags/${INPUT_TAG}"
echo "tag=${INPUT_TAG}" >> $GITHUB_OUTPUT
else
# For tag push events, use the ref directly
CHECKOUT_REF="${GITHUB_REF}"
fi
echo "ref=${CHECKOUT_REF}" >> $GITHUB_OUTPUT
- name: Verify GH_TA4J_REPO_TOKEN present
run: |
if [[ -z "${GH_TA4J_REPO_TOKEN:-}" ]]; then
echo "::error::GH_TA4J_REPO_TOKEN is required to create GitHub Releases."
exit 1
fi
- name: Checkout full history
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ steps.checkout_ref.outputs.ref }}
token: ${{ env.GH_TA4J_REPO_TOKEN }}
- name: Checkout workflow support files
uses: actions/checkout@v6
with:
fetch-depth: 1
ref: ${{ github.sha }}
path: workflow-support
token: ${{ env.GH_TA4J_REPO_TOKEN }}
- name: Normalize dry run input
id: dry_run
run: |
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
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
else
raw=""
dry_run=false
fi
echo "audit:dry_run_input_raw='${raw}'"
echo "audit:dry_run_normalized=$dry_run"
echo "dryRun=$dry_run" >> $GITHUB_OUTPUT
# ------------------------------------------------------------
# Determine Tag + Validate Release Notes File
# ------------------------------------------------------------
- name: Determine Version
id: version
run: |
echo "::group::Resolve GitHub Release tag and notes"
INPUT_TAG="${{ steps.checkout_ref.outputs.tag }}"
if [[ -n "$INPUT_TAG" ]]; then
TAG="$INPUT_TAG"
else
if [[ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]]; then
echo "Error: 'tag' input is required for workflow_dispatch runs." >&2
exit 1
fi
TAG="${GITHUB_REF##*/}"
fi
if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::GitHub Release tags must be bare SemVer major.minor.patch values. Got: $TAG"
exit 1
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
NOTES_FILE="release/${TAG}.md"
if [[ ! -f "$NOTES_FILE" ]]; then
echo "Release notes file $NOTES_FILE not found" >&2
exit 1
fi
echo "notes_file=$NOTES_FILE" >> $GITHUB_OUTPUT
echo "::endgroup::"
- name: Preflight release permissions (dry run)
if: steps.dry_run.outputs.dryRun == 'true'
id: token_preflight
env:
GH_TA4J_REPO_TOKEN: ${{ env.GH_TA4J_REPO_TOKEN }}
run: |
set -euo pipefail
python - <<'PY'
import json
import os
import sys
import urllib.request
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)
def viewer_permission(token: str) -> str:
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",
},
)
with urllib.request.urlopen(req) as resp:
data = json.load(resp)
if "errors" in data:
raise RuntimeError(data["errors"])
return data["data"]["repository"]["viewerPermission"]
def has_write(permission: str) -> bool:
return permission in ("WRITE", "MAINTAIN", "ADMIN")
def check_token(token, label):
if not token:
print(f"{label}:missing")
return None
try:
permission = viewer_permission(token)
print(f"{label}:{permission}")
return permission
except Exception as exc:
print(f"::warning::{label} permission check failed: {exc}")
return None
pat = os.environ.get("GH_TA4J_REPO_TOKEN", "")
pat_perm = check_token(pat, "GH_TA4J_REPO_TOKEN")
if not (pat_perm and has_write(pat_perm)):
print("::error::GH_TA4J_REPO_TOKEN lacks write permission to create a release.")
sys.exit(1)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"repo_token_permission={pat_perm}\n")
PY
# ------------------------------------------------------------
# Prepare Release Notes Body
# ------------------------------------------------------------
# ------------------------------------------------------------
# Build artifacts: core, examples, ALL modules
# Produces:
# - *.jar
# - *-javadoc.jar
# - *-sources.jar
# ------------------------------------------------------------
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 25
cache: maven
- name: Build all module artifacts
shell: bash
run: |
set -euo pipefail
echo "::group::Build GitHub Release artifacts"
max_attempts=3
attempt=1
while true; do
echo "audit:release_artifact_build attempt=${attempt}/${max_attempts}"
if mvn -B -DskipTests -Dgpg.skip=true -Pproduction-release package 2>&1 | tee release-artifact-build.log; then
break
fi
if ((attempt >= max_attempts)); then
echo "::error::Release artifact build failed after ${max_attempts} attempts."
exit 1
fi
backoff_seconds=$((attempt * 15))
echo "::warning::Release artifact build failed on attempt ${attempt}. Retrying in ${backoff_seconds}s."
sleep "${backoff_seconds}"
attempt=$((attempt + 1))
done
echo "::endgroup::"
- name: Validate release artifacts
id: artifacts
run: |
set -euo pipefail
echo "::group::Validate exact GitHub Release artifact manifest"
python3 workflow-support/scripts/release/release_helpers.py artifact-manifest \
--version "${{ steps.version.outputs.tag }}" \
--output artifact-manifest.txt \
--github-output "$GITHUB_OUTPUT" \
--strict
echo "::endgroup::"
- name: Dry-run summary
if: steps.dry_run.outputs.dryRun == 'true'
run: |
echo "DRY RUN ENABLED: build completed; release publish skipped."
echo "Tag: ${{ steps.version.outputs.tag }}"
echo "Release notes: ${{ steps.version.outputs.notes_file }}"
echo "GH_TA4J_REPO_TOKEN permission: ${{ steps.token_preflight.outputs.repo_token_permission }}"
echo "Artifacts:"
files="${{ steps.artifacts.outputs.files }}"
if [[ -z "${files}" ]]; then
echo " (none found)"
else
while IFS= read -r file; do
echo " - ${file#./}"
done <<< "${files}"
fi
# ------------------------------------------------------------
# Upload full artifact set to GitHub Release
# ------------------------------------------------------------
- name: Create GitHub Release
if: steps.dry_run.outputs.dryRun != 'true'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.version.outputs.tag }}
body_path: ${{ steps.version.outputs.notes_file }}
files: ${{ steps.artifacts.outputs.files }}
env:
GITHUB_TOKEN: ${{ env.GH_TA4J_REPO_TOKEN }}
- name: GitHub Release summary
if: always()
env:
DRY_RUN: ${{ steps.dry_run.outputs.dryRun }}
TAG: ${{ steps.version.outputs.tag }}
NOTES_FILE: ${{ steps.version.outputs.notes_file }}
run: |
if [ "${DRY_RUN:-false}" = "true" ]; then
mutation_plan="would create/update the GitHub Release and upload artifacts for ${TAG:-unknown}"
rerun_guidance="rerun github-release.yml with dryRun=false for this tag, or let the tag push path run"
else
mutation_plan="completed GitHub Release publication"
rerun_guidance="not needed for this non-dry-run GitHub Release run"
fi
cat > github-release-audit.json <<EOF
{
"dryRun": "${DRY_RUN:-}",
"tag": "${TAG:-}",
"notesFile": "${NOTES_FILE:-}",
"artifactManifest": "artifact-manifest.txt",
"mutationPlan": "${mutation_plan}",
"rerunGuidance": "${rerun_guidance}",
"runUrl": "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
}
EOF
{
echo "## GitHub Release"
echo ""
echo "- dry run: ${DRY_RUN:-unknown}"
echo "- tag: ${TAG:-unknown}"
echo "- notes file: ${NOTES_FILE:-unknown}"
echo "- artifact manifest: artifact-manifest.txt"
echo "- mutation plan: ${mutation_plan}"
echo "- rerun guidance: ${rerun_guidance}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload GitHub Release audit artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: github-release-audit-${{ github.run_id }}
path: |
github-release-audit.json
artifact-manifest.txt
release-artifact-build.log
if-no-files-found: ignore
retention-days: 14