chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
name: Draft release notes
|
||||
|
||||
# At release CUT (a vX.Y.Z tag is pushed → the "GitHub Release" workflow creates
|
||||
# the draft), prepare everything the release coordinator needs before they hit
|
||||
# Publish:
|
||||
#
|
||||
# 1. Open a PR to omnigent/main updating the granular CHANGELOG.md (harvested
|
||||
# from each merged PR's "## Changelog" section), so the draft's
|
||||
# "Full Changelog" link resolves before the release goes public.
|
||||
# 2. Synthesize concise, curated release notes (an Omnigent agent collapses the
|
||||
# merged PRs into ~4-5 themed highlights per section) and drop them into the
|
||||
# GitHub Release DRAFT body for the coordinator to edit.
|
||||
#
|
||||
# Why `workflow_run` (not extending github-release.yml): that workflow is
|
||||
# deliberately minimal — it runs NO project code, only `gh release create`, so a
|
||||
# malicious tagged commit can't execute anything. We keep that guarantee by
|
||||
# running the heavy work (LLM + git harvest) in this SEPARATE workflow, which
|
||||
# runs from the trusted default branch (workflow_run always does), never from the
|
||||
# tagged commit. Same "harvester runs from main" posture as autoformat-pr.yml.
|
||||
#
|
||||
# The LLM machinery (creds gate, Claude Code CLI, provider config, secret-scan,
|
||||
# token-minted-after-agent, artifact redaction) mirrors doc-sync.yml. The agent
|
||||
# only ever sees already-merged, released history.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["GitHub Release"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag/ref to (re)draft (head of the range), e.g. v0.3.0
|
||||
required: true
|
||||
type: string
|
||||
base:
|
||||
description: >-
|
||||
Optional range-start override (tag/branch/sha). Needed when `tag` is not
|
||||
a final vX.Y.Z. Providing it makes the run a preview unless dry_run=false.
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
description: >-
|
||||
Preview only:
|
||||
auto (default) - preview for dev/rc tags, real PR for final versions;
|
||||
true - print the generated notes, don't open a PR;
|
||||
false - open a real PR to CHANGELOG.md
|
||||
required: false
|
||||
type: choice
|
||||
options: [auto, "true", "false"]
|
||||
default: auto
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: draft-release-notes-${{ github.event.workflow_run.head_branch || inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
SOURCE_REPO: omnigent-ai/omnigent
|
||||
OMNIGENT_SKIP_WEB_UI: "true"
|
||||
UV_INDEX_URL: https://pypi.org/simple
|
||||
PIP_INDEX_URL: https://pypi.org/simple
|
||||
|
||||
jobs:
|
||||
draft:
|
||||
name: Harvest CHANGELOG and draft release notes
|
||||
if: >-
|
||||
github.repository == 'omnigent-ai/omnigent' &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
# --- Resolve the tag and decide whether to proceed (no code run yet) ---
|
||||
- name: Resolve tag and guard
|
||||
id: guard
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
# On tag push, workflow_run.head_branch is the tag name (v0.3.0).
|
||||
RUN_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
INPUT_BASE: ${{ inputs.base }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${INPUT_TAG:-$RUN_BRANCH}"
|
||||
base="${INPUT_BASE:-}"
|
||||
proceed=false; dry_run=false
|
||||
|
||||
# Does the tag look like a final release (vX.Y.Z, not rc/dev/alpha/beta)?
|
||||
is_version=true
|
||||
case "$tag" in
|
||||
v[0-9]*.[0-9]*.[0-9]*) ;;
|
||||
*) is_version=false ;;
|
||||
esac
|
||||
case "$tag" in
|
||||
*rc*|*dev*|*a[0-9]*|*b[0-9]*) is_version=false ;;
|
||||
esac
|
||||
|
||||
if [ "$EVENT_NAME" = "workflow_run" ]; then
|
||||
# Real release cut: strict — only a final version tag proceeds.
|
||||
[ "$is_version" = "true" ] && proceed=true
|
||||
else
|
||||
# Manual dispatch: proceed for a final version tag OR when a base
|
||||
# override is given (arbitrary-ref preview/real run).
|
||||
if [ "$is_version" = "true" ] || [ -n "$base" ]; then
|
||||
proceed=true
|
||||
fi
|
||||
# dry_run: `auto` previews for a non-version tag or a base override,
|
||||
# and does a real run for a plain version tag; true/false force it.
|
||||
case "$INPUT_DRY_RUN" in
|
||||
true) dry_run=true ;;
|
||||
false) dry_run=false ;;
|
||||
*) if [ "$is_version" != "true" ] || [ -n "$base" ]; then dry_run=true; fi ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# NOTE: we do NOT probe for the draft release here. This step runs with
|
||||
# the read-only GITHUB_TOKEN, and GitHub hides DRAFT releases from tokens
|
||||
# without push access — the probe would always come back empty and wrongly
|
||||
# report "no draft". Draft detection happens after the App token is minted
|
||||
# (see "Resolve draft release"), which can see drafts.
|
||||
|
||||
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "base=${base}" >> "$GITHUB_OUTPUT"
|
||||
echo "proceed=${proceed}" >> "$GITHUB_OUTPUT"
|
||||
echo "dry_run=${dry_run}" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved tag=${tag} base=${base:-<none>} proceed=${proceed} dry_run=${dry_run}" \
|
||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Trusted default branch, full history + tags for the range computation.
|
||||
- name: Checkout omnigent (main)
|
||||
if: steps.guard.outputs.proceed == 'true'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
if: steps.guard.outputs.proceed == 'true'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
# --- 1) Harvest CHANGELOG.md + the mechanical scaffold + agent input ---
|
||||
- name: Harvest changelog and PR material
|
||||
id: harvest
|
||||
if: steps.guard.outputs.proceed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ steps.guard.outputs.tag }}
|
||||
BASE: ${{ steps.guard.outputs.base }}
|
||||
DRY_RUN: ${{ steps.guard.outputs.dry_run }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# generate.py orders CHANGELOG.md by PEP 440 (packaging). This step runs
|
||||
# bare python3 (before uv sync), so ensure packaging is importable.
|
||||
python3 -m pip install --quiet --disable-pip-version-check packaging
|
||||
args=(--tag "$TAG" --repo "$SOURCE_REPO"
|
||||
--draft-notes-out /tmp/mechanical_notes.md
|
||||
--pr-list-out /tmp/pr_list.txt
|
||||
--section-out /tmp/section.md)
|
||||
[ -n "${BASE:-}" ] && args+=(--base "$BASE")
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
# Preview only — render, don't touch CHANGELOG.md.
|
||||
args+=(--no-changelog-update)
|
||||
else
|
||||
args+=(--changelog-file CHANGELOG.md)
|
||||
fi
|
||||
python3 .github/scripts/changelog/generate.py "${args[@]}"
|
||||
# The mechanical scaffold is the fallback release-notes body.
|
||||
cp /tmp/mechanical_notes.md /tmp/release_notes.md
|
||||
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
{
|
||||
echo "## Preview — CHANGELOG.md section for \`${TAG}\`"
|
||||
echo '```markdown'; cat /tmp/section.md; echo '```'
|
||||
echo "## Preview — mechanical draft notes"
|
||||
echo '```markdown'; cat /tmp/mechanical_notes.md; echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
# --- 2) AI synthesis (primary; degrades to the mechanical scaffold) ---
|
||||
- name: Check LLM credentials
|
||||
id: creds
|
||||
if: steps.guard.outputs.proceed == 'true'
|
||||
env:
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
run: |
|
||||
if [ -z "${LLM_API_KEY:-}" ]; then
|
||||
echo "::warning::No LLM credentials — using the mechanical draft scaffold."
|
||||
echo "available=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::add-mask::${LLM_API_KEY}"
|
||||
echo "available=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Set up uv
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Cache virtualenv
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('uv.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
run: uv sync --extra all --extra dev
|
||||
|
||||
- name: Install Claude Code CLI
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
env:
|
||||
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
|
||||
run: |
|
||||
mkdir -p "${GITHUB_WORKSPACE}/.cc-cli" && cd "${GITHUB_WORKSPACE}/.cc-cli"
|
||||
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.170
|
||||
node node_modules/@anthropic-ai/claude-code/install.cjs
|
||||
echo "${GITHUB_WORKSPACE}/.cc-cli/node_modules/.bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Write Omnigent provider config
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
env:
|
||||
GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }}
|
||||
run: |
|
||||
mkdir -p "$HOME/.omnigent"
|
||||
python3 -c "
|
||||
import pathlib, os, json
|
||||
gw = os.environ['GATEWAY_BASE_URL']
|
||||
cfg = {'providers': {'databricks-gateway': {
|
||||
'kind': 'gateway', 'default': ['anthropic'],
|
||||
'anthropic': {
|
||||
'base_url': gw + '/anthropic',
|
||||
'api_key_ref': 'env:LLM_API_KEY',
|
||||
'models': {'default': 'databricks-claude-opus-4-8'},
|
||||
}}}}
|
||||
pathlib.Path.home().joinpath('.omnigent', 'config.yaml').write_text(json.dumps(cfg, indent=2))
|
||||
"
|
||||
|
||||
- name: Build drafter prompt
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
env:
|
||||
TAG: ${{ steps.guard.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 -u <<'PYEOF'
|
||||
import os, pathlib
|
||||
tag = os.environ["TAG"]
|
||||
# The agent is tools-less, so its input must be inline — but `omnigent run
|
||||
# -p` passes the whole prompt as one argv string, capped at ~128 KiB on
|
||||
# Linux (MAX_ARG_STRLEN). Cap the PR list well under that; the mechanical
|
||||
# scaffold already covers everything, so a partial list still drafts.
|
||||
MAX = 100_000
|
||||
pr_list = pathlib.Path("/tmp/pr_list.txt").read_text(encoding="utf-8", errors="replace")
|
||||
mech = pathlib.Path("/tmp/mechanical_notes.md").read_text(encoding="utf-8", errors="replace")
|
||||
truncated = len(pr_list) > MAX
|
||||
pr_list = pr_list[:MAX]
|
||||
note = ("\n> NOTE: the PR list was truncated — theme what's visible and keep the "
|
||||
"mechanical draft's coverage.\n" if truncated else "")
|
||||
prompt = f"""Draft the curated release notes for {tag}.
|
||||
{note}
|
||||
## Merged PRs (number, title, and author changelog entries)
|
||||
{pr_list}
|
||||
|
||||
## Mechanical draft (raw material — curate, don't copy verbatim)
|
||||
{mech}
|
||||
|
||||
Produce the RELEASE_NOTES block per your instructions."""
|
||||
pathlib.Path("/tmp/draft_prompt.txt").write_text(prompt)
|
||||
PYEOF
|
||||
|
||||
- name: Run release-notes drafter
|
||||
id: draft
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
env:
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
prompt="$(cat /tmp/draft_prompt.txt)"
|
||||
uv run --project "${GITHUB_WORKSPACE}" omnigent run \
|
||||
"${GITHUB_WORKSPACE}/.github/agents/release-notes-drafter" \
|
||||
-p "$prompt" --no-session \
|
||||
2>draft-stderr.log | tee /tmp/draft_out.txt \
|
||||
|| { echo "::warning::drafter exited non-zero — keeping mechanical draft"; cat draft-stderr.log; }
|
||||
|
||||
- name: Scan drafter output for secrets
|
||||
if: steps.draft.outcome == 'success'
|
||||
env:
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "${LLM_API_KEY:-}" ] && grep -qF "$LLM_API_KEY" /tmp/draft_out.txt 2>/dev/null; then
|
||||
echo "::error::Drafter output contains LLM_API_KEY — aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Extract synthesized notes (fall back to mechanical)
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 -u <<'PYEOF'
|
||||
import pathlib, re
|
||||
raw = pathlib.Path("/tmp/draft_out.txt").read_text(encoding="utf-8", errors="replace") \
|
||||
if pathlib.Path("/tmp/draft_out.txt").is_file() else ""
|
||||
m = re.search(r"<!--\s*RELEASE_NOTES\s*-->(.*?)<!--\s*/RELEASE_NOTES\s*-->", raw, re.DOTALL)
|
||||
notes = (m.group(1).strip() if m else "")
|
||||
if notes:
|
||||
pathlib.Path("/tmp/release_notes.md").write_text(notes + "\n")
|
||||
print("Using AI-synthesized release notes.")
|
||||
else:
|
||||
print("::warning::No RELEASE_NOTES block parsed — keeping mechanical draft.")
|
||||
PYEOF
|
||||
|
||||
# --- 3) Mint the write-token — ONLY now, after the agent has run ---
|
||||
- name: Mint App token (omnigent)
|
||||
id: app-token
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.guard.outputs.dry_run != 'true' && vars.OMNIGENT_BOT_APP_ID != ''
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
app-id: ${{ vars.OMNIGENT_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.OMNIGENT_BOT_APP_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: omnigent
|
||||
|
||||
# Find the DRAFT release for this tag using the App token (push access) — a
|
||||
# read-only token can't see drafts. Match by tag_name over the release list:
|
||||
# GitHub's get-by-tag REST endpoint 404s on drafts (their tag isn't "real"
|
||||
# until published), so only a list-and-filter finds them. Sets:
|
||||
# is_draft — true only when a matching UNPUBLISHED draft exists (so we
|
||||
# never clobber notes a maintainer already published).
|
||||
# release_id — numeric id to edit by (editing by tag would 404 on a draft).
|
||||
- name: Resolve draft release
|
||||
id: release
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.guard.outputs.dry_run != 'true' && steps.app-token.outputs.token != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
TAG: ${{ steps.guard.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Read TAG via jq's `env`, not by interpolating it into the jq program —
|
||||
# a tag containing `"` or jq syntax would otherwise alter the filter.
|
||||
# (gh api's built-in --jq has no --arg; env keeps the value as data.)
|
||||
match="$(gh api "repos/${SOURCE_REPO}/releases" --paginate \
|
||||
--jq 'map(select(.tag_name == env.TAG)) | first // empty')"
|
||||
is_draft=false; release_id=""
|
||||
if [ -n "$match" ]; then
|
||||
is_draft="$(printf '%s' "$match" | jq -r '.draft')"
|
||||
release_id="$(printf '%s' "$match" | jq -r '.id')"
|
||||
fi
|
||||
if [ "$is_draft" != "true" ]; then
|
||||
echo "::notice::No unpublished draft release found for ${TAG} — leaving release notes untouched (the CHANGELOG PR still runs)."
|
||||
fi
|
||||
echo "is_draft=${is_draft}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_id=${release_id}" >> "$GITHUB_OUTPUT"
|
||||
echo "Draft release for ${TAG}: is_draft=${is_draft} release_id=${release_id:-<none>}" \
|
||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# --- 4) Open/update the CHANGELOG.md PR ---
|
||||
- name: Open or update the CHANGELOG.md PR
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.guard.outputs.dry_run != 'true' && steps.app-token.outputs.token != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
SITE_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
TAG: ${{ steps.guard.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$(git status --porcelain -- CHANGELOG.md)" ]; then
|
||||
echo "CHANGELOG.md already up to date for ${TAG} — nothing to do." \
|
||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
BRANCH="auto/changelog/${TAG}"
|
||||
git config user.name "omnigent-ci[bot]"
|
||||
git config user.email "294685417+omnigent-ci[bot]@users.noreply.github.com"
|
||||
# No credentials persisted in .git/config (the unsandboxed agent ran
|
||||
# earlier); push via the token URL, which GitHub masks in logs.
|
||||
PUSH_URL="https://x-access-token:${SITE_TOKEN}@github.com/${SOURCE_REPO}.git"
|
||||
git switch -C "$BRANCH"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "docs(changelog): record ${TAG}"
|
||||
git push --force "$PUSH_URL" "$BRANCH"
|
||||
|
||||
if [ -n "$(gh pr list --repo "$SOURCE_REPO" --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
|
||||
echo "CHANGELOG PR already open for ${BRANCH} — force-push updated it."
|
||||
exit 0
|
||||
fi
|
||||
body="$(printf 'Records **%s** in `CHANGELOG.md`, harvested from the `## Changelog` section of each merged PR. Merge as part of cutting the release so the draft notes '"'"'Full Changelog'"'"' link resolves.\n\nGenerated by `.github/workflows/draft-release-notes.yml`.' "$TAG")"
|
||||
gh pr create \
|
||||
--repo "$SOURCE_REPO" \
|
||||
--base main \
|
||||
--head "$BRANCH" \
|
||||
--title "docs(changelog): record ${TAG}" \
|
||||
--body "$body"
|
||||
|
||||
# --- 5) Enrich the GitHub Release DRAFT body (only while still a draft) ---
|
||||
- name: Enrich the release draft body
|
||||
if: steps.guard.outputs.proceed == 'true' && steps.release.outputs.is_draft == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
TAG: ${{ steps.guard.outputs.tag }}
|
||||
RELEASE_ID: ${{ steps.release.outputs.release_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Always end the notes with the community thanks. The AI drafter curates
|
||||
# freely (and can drop a hand-added line), so this is appended here rather
|
||||
# than via the prompt — every release, AI-drafted or mechanical fallback,
|
||||
# gets it. Idempotent, and placed just before the trailing "Full Changelog:"
|
||||
# link to match the layout of prior releases.
|
||||
python3 - <<'PYEOF'
|
||||
import pathlib
|
||||
NOTE = (
|
||||
"### 💜 Thanks to our community\n\n"
|
||||
"This release was shaped by the people who filed issues, opened PRs, and "
|
||||
"talked through feature requests with us on our Discord! Thank you for "
|
||||
"building omnigent with us, keep the bug reports, ideas and contributions "
|
||||
"coming :)"
|
||||
)
|
||||
path = pathlib.Path("/tmp/release_notes.md")
|
||||
text = path.read_text(encoding="utf-8").rstrip("\n")
|
||||
if "Thanks to our community" not in text:
|
||||
idx = text.find("\nFull Changelog:")
|
||||
if idx != -1:
|
||||
head, tail = text[:idx].rstrip("\n"), text[idx:].lstrip("\n")
|
||||
text = f"{head}\n\n{NOTE}\n\n{tail}"
|
||||
else:
|
||||
text = f"{text}\n\n{NOTE}"
|
||||
path.write_text(text + "\n", encoding="utf-8")
|
||||
PYEOF
|
||||
# github-release.yml seeds only a short placeholder body (no
|
||||
# auto-generated notes), so replace it wholesale with the curated notes.
|
||||
# Edit by release ID: a draft release can't be addressed by tag (the
|
||||
# get/edit-by-tag REST endpoint 404s until the release is published).
|
||||
gh api --method PATCH "repos/${SOURCE_REPO}/releases/${RELEASE_ID}" \
|
||||
--field body=@/tmp/release_notes.md > /dev/null
|
||||
echo "Enriched the ${TAG} release draft with curated notes + community note." \
|
||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ::add-mask:: redacts rendered logs, not artifact files — scrub the key
|
||||
# from artifacts (incl. the unscanned stderr) before upload.
|
||||
- name: Redact secrets from artifacts
|
||||
if: always() && steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
|
||||
env:
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "${LLM_API_KEY:-}" ] || exit 0
|
||||
python3 - <<'PYEOF'
|
||||
import os, pathlib
|
||||
key = os.environ.get("LLM_API_KEY", "")
|
||||
for f in ["draft-stderr.log", "/tmp/draft_out.txt", "/tmp/draft_prompt.txt",
|
||||
"/tmp/release_notes.md"]:
|
||||
p = pathlib.Path(f)
|
||||
if not p.is_file() or not key:
|
||||
continue
|
||||
t = p.read_text(encoding="utf-8", errors="replace")
|
||||
if key in t:
|
||||
p.write_text(t.replace(key, "***REDACTED***"), encoding="utf-8")
|
||||
print(f"redacted key from {f}")
|
||||
PYEOF
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: always() && steps.guard.outputs.proceed == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: draft-release-notes-${{ steps.guard.outputs.tag }}-${{ github.run_id }}
|
||||
path: |
|
||||
draft-stderr.log
|
||||
/tmp/draft_out.txt
|
||||
/tmp/release_notes.md
|
||||
/tmp/mechanical_notes.md
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
Reference in New Issue
Block a user