Generate release summary #44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Generate release summary" | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| scope: | |
| description: "Comma-separated repos (owner/repo) OR 'org:armbian'" | |
| required: false | |
| default: "org:armbian" | |
| tz: | |
| description: "Timezone (IANA)" | |
| required: false | |
| default: "Europe/Ljubljana" | |
| period: | |
| description: "Digest period (weekly by default). Choose 'monthly' or 'quarterly' for larger windows." | |
| required: false | |
| default: "weekly" | |
| publish_release: | |
| description: "Publish a GitHub release (true/false)." | |
| required: false | |
| default: "true" | |
| schedule: | |
| - cron: "5 6 * * MON" # weekly, Mondays 06:05 UTC | |
| permissions: | |
| contents: read | |
| jobs: | |
| pr-digest: | |
| name: "Get PR's" | |
| runs-on: ubuntu-latest | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_TOKEN: ${{ secrets.AI_MODELS }} | |
| SCOPE: ${{ inputs.scope || 'armbian/build,armbian/configng' }} | |
| TZ: ${{ inputs.tz || 'Europe/Ljubljana' }} | |
| PERIOD: ${{ inputs.period || 'weekly' }} | |
| RELEASE_ENABLED: ${{ inputs.publish_release || 'true' }} | |
| steps: | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.10' | |
| - name: Install OpenAI SDK | |
| run: pip install 'openai>=1.0.0' | |
| - name: Ensure dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y jq | |
| - name: Compute time window (UTC) from period | |
| id: when | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| export PERIOD="${PERIOD:-weekly}" | |
| UNTIL_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| if [[ "$PERIOD" == "monthly" ]]; then | |
| SINCE_UTC="$(date -u -d '1 month ago' +%Y-%m-%dT%H:%M:%SZ)" | |
| LABEL="Monthly digest" | |
| elif [[ "$PERIOD" == "quarterly" ]]; then | |
| SINCE_UTC="$(date -u -d '3 months ago' +%Y-%m-%dT%H:%M:%SZ)" | |
| LABEL="Quarterly digest" | |
| else | |
| SINCE_UTC="$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)" | |
| LABEL="Weekly digest" | |
| fi | |
| echo "UNTIL_UTC=$UNTIL_UTC" >> "$GITHUB_ENV" | |
| echo "SINCE_UTC=$SINCE_UTC" >> "$GITHUB_ENV" | |
| echo "LABEL=$LABEL" >> "$GITHUB_ENV" | |
| - name: Prepare workspace | |
| run: mkdir -p out | |
| - name: Write helper script | |
| shell: bash | |
| run: | | |
| cat > pr_fetch.sh <<'EOF' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| scope="${1:?}" # "org:armbian" or "owner/repo,owner/repo2" | |
| since="${2:?}" # ISO UTC | |
| until="${3:?}" # ISO UTC | |
| outfile="${4:?}" # path | |
| collect() { | |
| local q="$1" | |
| gh api -X GET search/issues \ | |
| -f q="$q" \ | |
| -f sort="updated" -f order="desc" \ | |
| -f per_page=100 --paginate \ | |
| -q '.items[] | {number: .number, repo: (.repository_url | sub("^.*/repos/";""))}' | |
| } | |
| tmp="$(mktemp)" | |
| if [[ "$scope" == org:* ]]; then | |
| org="${scope#org:}" | |
| q="org:${org} is:pr is:merged merged:${since}..${until} archived:false" | |
| collect "$q" > "$tmp" | |
| else | |
| IFS=',' read -r -a repos <<< "$scope" | |
| : > "$tmp" | |
| for r in "${repos[@]}"; do | |
| r_trim="$(echo "$r" | xargs)" | |
| q="repo:${r_trim} is:pr is:merged merged:${since}..${until}" | |
| collect "$q" >> "$tmp" | |
| done | |
| fi | |
| # De-dup and enrich, then output TSV sorted alphabetically by title (case-insensitive) | |
| jq -s 'unique_by(.repo + "|" + (.number|tostring))' "$tmp" | jq -c '.[]' | \ | |
| while read -r row; do | |
| repo=$(echo "$row" | jq -r .repo) | |
| num=$(echo "$row" | jq -r .number) | |
| pr=$(gh api "repos/${repo}/pulls/${num}") | |
| title=$(jq -r .title <<<"$pr") | |
| author=$(jq -r .user.login <<<"$pr") | |
| pr_url=$(jq -r .html_url <<<"$pr") | |
| # Skip bots and worker accounts | |
| if [[ "$author" == "github-actions[bot]" || "$author" == "dependabot[bot]" || "$author" == "armbianworker" || "$author" == "armbianworker[bot]" ]]; then | |
| continue | |
| fi | |
| # Output tab-separated: title, author, repo, pr_number, pr_url | |
| printf "%s\t%s\t%s\t%s\t%s\n" "$title" "$author" "$repo" "$num" "$pr_url" | |
| done | LC_ALL=C sort -f -t $'\t' -k1,1 > "$outfile" | |
| EOF | |
| chmod +x pr_fetch.sh | |
| - name: Fetch merged PRs for selected period | |
| run: ./pr_fetch.sh "$SCOPE" "$SINCE_UTC" "$UNTIL_UTC" out/pr-digest.tsv | |
| - name: "Write Markdown digest" | |
| shell: bash | |
| run: | | |
| echo "" >> summary.md | |
| echo "## " >> summary.md | |
| if [[ ! -s out/pr-digest.tsv ]]; then | |
| echo "_No merged PRs in this period._" >> summary.md | |
| else | |
| while IFS=$'\t' read -r title author repo num pr_url; do | |
| echo "* ${title}. by @${author} in [${repo}#${num}](${pr_url})" >> summary.md | |
| done < out/pr-digest.tsv | |
| fi | |
| echo "# " >> summary.md | |
| echo "<a href='https://blog.armbian.com/#/portal/signup' target='_blank'> | |
| <img src='https://img.shields.io/badge/Subscribe-blog.armbian.com-red?style=for-the-badge&logo=rss' alt='Subscribe to Blog'/></a>" >> summary.md | |
| echo "<p><br>Stay up to date with the latest Armbian news, development highlights, and tips — delivered straight to your inbox." >> summary.md | |
| - name: Upload raw data (artifacts) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pr-digest | |
| path: out/ | |
| if-no-files-found: warn | |
| - name: "Checkout OS repository to get version" | |
| uses: actions/checkout@v5 | |
| with: | |
| repository: armbian/os | |
| fetch-depth: 0 | |
| clean: false | |
| path: os | |
| - name: "Read version from nightly or stable based on period" | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${PERIOD:-weekly}" == "monthly" || "${PERIOD:-weekly}" == "quarterly" ]]; then | |
| FILE="os/stable.json" | |
| else | |
| FILE="os/nightly.json" | |
| fi | |
| if [[ ! -f "$FILE" ]]; then | |
| echo "::error file=$FILE::Version file not found" | |
| exit 1 | |
| fi | |
| VERSION=$(jq -r '.version' "$FILE") | |
| echo "VERSION_OVERRIDE=${VERSION}" >> "$GITHUB_ENV" | |
| - name: "Write a short journalistic intro with AI" | |
| if: ${{ env.PERIOD == 'weekly' }} | |
| run: | | |
| set -euo pipefail | |
| python3 <<'PY' | |
| import os | |
| from openai import OpenAI | |
| token = os.environ["GITHUB_TOKEN"] | |
| label = os.environ["LABEL"] | |
| with open("summary.md", "r", encoding="utf-8") as f: | |
| content = f.read().strip() | |
| client = OpenAI(base_url="https://models.github.ai/inference", api_key=token) | |
| prompt = ( | |
| "Read the following Markdown changelog and write a short journalistic intro paragraph (5–7 sentences) " | |
| "that summarizes the main activity. Be concise, professional, and factual. Output only the intro.\n\n" | |
| f"{content}" | |
| ) | |
| resp = client.chat.completions.create( | |
| model="openai/gpt-4.1", | |
| messages=[ | |
| {"role": "system", "content": "You are a " + label + "digest editor."}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| temperature=0.3, | |
| top_p=1.0, | |
| ) | |
| intro = resp.choices[0].message.content.strip() | |
| with open("summary.md", "w", encoding="utf-8") as f: | |
| f.write(intro + "\n\n" + content) | |
| print("Prepended AI intro:") | |
| print(intro) | |
| PY | |
| cat summary.md >> "$GITHUB_STEP_SUMMARY" | |
| - uses: ncipollo/release-action@v1 | |
| if: ${{ env.RELEASE_ENABLED == 'true' }} | |
| with: | |
| owner: 'armbian' | |
| repo: 'build' | |
| tag: "v${{ env.VERSION_OVERRIDE }}" | |
| name: "${{ env.LABEL }}" | |
| generateReleaseNotes: "false" | |
| prerelease: "false" | |
| makeLatest: "true" | |
| bodyFile: "summary.md" | |
| allowUpdates: "true" | |
| skipIfReleaseExists: "true" | |
| token: ${{ secrets.RELEASE_TOKEN }} |