Skip to content

Nightly

Nightly #96

Workflow file for this run

name: Nightly
on:
schedule:
- cron: '0 17 * * *' # 맀일 UTC 17:00 (KST 02:00)
workflow_dispatch:
concurrency:
group: nightly
cancel-in-progress: true
jobs:
# ──────────────────────────────────────────────
# JOB 1 : 정적 뢄석 (Release variant κΈ°μ€€)
# ──────────────────────────────────────────────
static-analysis:
name: Static Analysis (Release)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Generate local.properties
run: |
write_local_property() {
local name="$1"
local value="$2"
if [ -z "$value" ]; then
echo "Missing required local.properties secret: $name"
exit 1
fi
if [[ "$value" == ${name}* ]]; then
value="${value#*=}"
value="${value# }"
fi
if [[ "$value" == \"*\" ]]; then
value="${value#\"}"
value="${value%\"}"
fi
printf '%s = "%s"\n' "$name" "$value" >> ./local.properties
}
: > ./local.properties
write_local_property "PROD_BASE_URL" "$PROD_BASE_URL"
write_local_property "DEV_BASE_URL" "$DEV_BASE_URL"
write_local_property "TERMS_URL" "$TERMS_URL"
write_local_property "REPORT_FORM_URL" "$REPORT_FORM_URL"
env:
PROD_BASE_URL: ${{ secrets.PROD_BASE_URL }}
DEV_BASE_URL: ${{ secrets.DEV_BASE_URL }}
TERMS_URL: ${{ secrets.TERMS_URL }}
REPORT_FORM_URL: ${{ secrets.REPORT_FORM_URL }}
- name: Create google-services.json
run: |
mkdir -p ${{ github.workspace }}/app
SECRET='${{ secrets.GOOGLE_SERVICES_JSON }}'
TARGET='${{ github.workspace }}/app/google-services.json'
if ! printf '%s' "$SECRET" | jq -e . > /dev/null 2>&1; then
echo "Invalid GOOGLE_SERVICES_JSON secret: must be raw JSON content from app/google-services.json"
exit 1
fi
printf '%s' "$SECRET" > "$TARGET"
jq -e . "$TARGET" > /dev/null
- name: Run static analysis
run: ./gradlew ktlintCheck detekt lintRelease --continue
- name: Merge detekt SARIF reports
if: always()
run: |
python3 - <<'EOF'
import json, glob
results, rules, tool = [], {}, None
for path in sorted(glob.glob('./**/reports/detekt/detekt.sarif', recursive=True)):
with open(path) as f:
data = json.load(f)
for run in data.get('runs', []):
if tool is None:
tool = run.get('tool', {})
for rule in run.get('tool', {}).get('driver', {}).get('rules', []):
rules[rule['id']] = rule
results.extend(run.get('results', []))
if tool is None:
tool = {"driver": {"name": "detekt", "rules": []}}
tool.setdefault('driver', {})['rules'] = list(rules.values())
merged = {
"version": "2.1.0",
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": [{"tool": tool, "results": results}]
}
import os; os.makedirs('build/detekt-sarif', exist_ok=True)
with open('build/detekt-sarif/merged.sarif', 'w') as f:
json.dump(merged, f)
print(f"Merged {len(results)} result(s) from {len(glob.glob('./**/reports/detekt/detekt.sarif', recursive=True))} module(s)")
EOF
- name: Upload detekt SARIF to GitHub Security
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: build/detekt-sarif/merged.sarif
category: detekt-nightly
- name: Collect lint reports
if: always()
run: |
mkdir -p build/lint-reports
find . -name 'lint-results-*.html' \
-exec cp {} build/lint-reports/ \;
find . -name 'lint-results-*.sarif' \
-exec cp {} build/lint-reports/ \;
- name: Upload lint HTML report
if: always()
uses: actions/upload-artifact@v7
with:
name: lint-html-report-release
path: build/lint-reports/*.html
retention-days: 30
- name: Upload lint SARIF to GitHub Security
if: always() && hashFiles('build/lint-reports/*.sarif') != ''
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: build/lint-reports
category: lint-nightly
# ──────────────────────────────────────────────
# JOB 2 : Release λΉŒλ“œ + Compose Metrics
# ──────────────────────────────────────────────
release-build:
name: Release Build & Compose Metrics
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Generate local.properties
run: |
write_local_property() {
local name="$1"
local value="$2"
if [ -z "$value" ]; then
echo "Missing required local.properties secret: $name"
exit 1
fi
if [[ "$value" == ${name}* ]]; then
value="${value#*=}"
value="${value# }"
fi
if [[ "$value" == \"*\" ]]; then
value="${value#\"}"
value="${value%\"}"
fi
printf '%s = "%s"\n' "$name" "$value" >> ./local.properties
}
: > ./local.properties
write_local_property "PROD_BASE_URL" "$PROD_BASE_URL"
write_local_property "DEV_BASE_URL" "$DEV_BASE_URL"
write_local_property "TERMS_URL" "$TERMS_URL"
write_local_property "REPORT_FORM_URL" "$REPORT_FORM_URL"
env:
PROD_BASE_URL: ${{ secrets.PROD_BASE_URL }}
DEV_BASE_URL: ${{ secrets.DEV_BASE_URL }}
TERMS_URL: ${{ secrets.TERMS_URL }}
REPORT_FORM_URL: ${{ secrets.REPORT_FORM_URL }}
- name: Create google-services.json
run: |
mkdir -p ${{ github.workspace }}/app
SECRET='${{ secrets.GOOGLE_SERVICES_JSON }}'
TARGET='${{ github.workspace }}/app/google-services.json'
if ! printf '%s' "$SECRET" | jq -e . > /dev/null 2>&1; then
echo "Invalid GOOGLE_SERVICES_JSON secret: must be raw JSON content from app/google-services.json"
exit 1
fi
printf '%s' "$SECRET" > "$TARGET"
jq -e . "$TARGET" > /dev/null
- name: Decode and restore keystore
run: |
KEYSTORE_PATH="${RUNNER_TEMP}/dmsStore.jks"
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d ' \n\r' | base64 --decode > "$KEYSTORE_PATH"
echo "KEYSTORE_PATH=$KEYSTORE_PATH" >> $GITHUB_ENV
# ── assembleRelease + Compose Metrics ───────────
- name: Build Release APK with Compose metrics
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: ./gradlew assembleRelease -PenableMultiModuleComposeReports=true
- name: Upload Release APK
uses: actions/upload-artifact@v7
with:
name: release-apk-${{ github.run_number }}
path: app/build/outputs/apk/release/*.apk
retention-days: 30
- name: Cache mendable.jar
id: mendable-cache
uses: actions/cache@v5
with:
path: tools/mendable.jar
key: mendable-jar-v0.7.0
- name: Download mendable.jar
if: steps.mendable-cache.outputs.cache-hit != 'true'
run: |
mkdir -p tools
curl -fL --retry 3 --retry-delay 2 \
-o tools/mendable.jar \
"https://github.com/jayasuryat/mendable/releases/download/v0.7.0/mendable.jar"
test -s tools/mendable.jar
- name: Generate mendable HTML report
run: |
mkdir -p build/mendable-output
java -jar tools/mendable.jar \
-i build/compose_metrics \
-sr \
-o build/mendable-output \
-oName mendable-report \
-eType HTML \
-rType ALL
- name: Generate mendable JSON report (warnings only)
run: |
java -jar tools/mendable.jar \
-i build/compose_metrics \
-sr \
-o build/mendable-output \
-oName mendable-warnings \
-eType JSON \
-rType WITH_WARNINGS
- name: Check Compose metrics (skippable ratio)
run: |
python3 scripts/check_compose_metrics.py \
build/mendable-output/mendable-warnings.json \
--threshold 100
- name: Upload mendable HTML report
if: always()
uses: actions/upload-artifact@v7
with:
name: compose-metrics-report-${{ github.run_number }}
path: build/mendable-output/mendable-report.html
retention-days: 30
# ──────────────────────────────────────────────
# JOB 3 : κ²°κ³Ό μ•Œλ¦Ό (항상 μ‹€ν–‰)
# ──────────────────────────────────────────────
notify:
name: Notify Result
runs-on: ubuntu-latest
needs: [ static-analysis, release-build ]
if: always()
steps:
- name: Determine result
id: result
run: |
STATIC="${{ needs.static-analysis.result }}"
BUILD="${{ needs.release-build.result }}"
if [[ "$STATIC" == "success" && "$BUILD" == "success" ]]; then
echo "status=success" >> $GITHUB_OUTPUT
echo "emoji=βœ…" >> $GITHUB_OUTPUT
echo "label=Nightly λΉŒλ“œ 성곡" >> $GITHUB_OUTPUT
echo "color=good" >> $GITHUB_OUTPUT
echo "color_int=3066993" >> $GITHUB_OUTPUT
else
echo "status=failure" >> $GITHUB_OUTPUT
echo "emoji=❌" >> $GITHUB_OUTPUT
echo "color=danger" >> $GITHUB_OUTPUT
echo "color_int=15158332" >> $GITHUB_OUTPUT
FAILED=""
[[ "$STATIC" != "success" ]] && FAILED="${FAILED}Static Analysis(${STATIC}) "
[[ "$BUILD" != "success" ]] && FAILED="${FAILED}Release Build(${BUILD})"
echo "label=Nightly λΉŒλ“œ μ‹€νŒ¨ β€” ${FAILED}" >> $GITHUB_OUTPUT
fi
- name: Notify Slack
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "${{ steps.result.outputs.emoji }} *${{ steps.result.outputs.label }}*",
"attachments": [{
"color": "${{ steps.result.outputs.color }}",
"fields": [
{ "title": "Static Analysis", "value": "${{ needs.static-analysis.result }}", "short": true },
{ "title": "Release Build", "value": "${{ needs.release-build.result }}", "short": true },
{ "title": "Run",
"value": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
"short": false }
]
}]
}
- name: Notify Discord
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
LABEL: ${{ steps.result.outputs.label }}
COLOR: ${{ steps.result.outputs.color_int }}
STATIC_RESULT: ${{ needs.static-analysis.result }}
BUILD_RESULT: ${{ needs.release-build.result }}
run: |
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{
\"embeds\": [{
\"title\": \"$LABEL\",
\"color\": $COLOR,
\"fields\": [
{ \"name\": \"Static Analysis\", \"value\": \"$STATIC_RESULT\", \"inline\": true },
{ \"name\": \"Release Build\", \"value\": \"$BUILD_RESULT\", \"inline\": true },
{ \"name\": \"Run\",
\"value\": \"[View Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\" }
]
}]
}"