mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
## Summary This pull request restructures Gradle dependency caching across the GitHub Actions workflows. The central `gradle-cache-prime` job is responsible for preparing the shared backend Gradle cache. Reusable workflows restore that shared cache without writing to the same key, while independently triggered workflows use isolated cache namespaces. ## What changed ### Shared Gradle cache - Added a stable `gradle-v1-` cache namespace for the shared backend cache. - The cache key includes the runner OS, runner architecture, JDK version, and the relevant Gradle configuration files. - The cache key is calculated before Gradle runs and reused for the later save step. - The prime job performs a lookup first and resolves backend dependencies only when the exact cache is missing. - This prevents Gradle or Spotless changes during the prime step from producing a different save key from the key used by downstream jobs. ### Reusable workflows - Backend, OpenAPI, license, Docker, E2E, and migration workflows restore the shared cache instead of writing to the shared key. - The backend build matrix includes `matrix.jdk-version` in its cache key. - Enterprise, Tauri, and generated-model workflows support the `use_shared_cache` boolean input. - When `use_shared_cache` is enabled, those workflows restore the shared cache. - When it is disabled, they use workflow-specific cache namespaces. ### Independent workflows Independent workflows now use separate cache prefixes, including: - `gradle-license-report-v1-` - `gradle-swagger-v1-` - `gradle-push-docker-v1-` - `gradle-tauri-releases-v1-` - `gradle-deploy-pr-v1-` - `gradle-playwright-e2e-v1-` - `gradle-generated-models-v1-` This prevents them from creating or affecting the shared backend cache before the prime job. ### Build and E2E flow - Removed the `-PnoSpotless` option from the central Gradle dependency-resolution command. - Removed the separate Gradle dependency prime/retry logic from the live E2E workflow. - Connected the Tauri build and generated-models check to the central cache-prime job. ## Motivation Previously, multiple workflows could use and save the same Gradle cache key independently. The first workflow to save the cache could therefore determine its contents, even if it had resolved a different or incomplete set of dependencies. The cache key was also evaluated after some Gradle tasks had run. If Gradle or Spotless modified a file covered by `hashFiles(...)`, the save key could differ from the restore key used by downstream jobs. This change gives the shared cache a single owner, isolates workflow-specific caches, and makes cache usage deterministic across the CI pipeline. ## Expected result - `gradle-cache-prime` is the single writer for the shared backend Gradle cache. - Downstream jobs restore the same cache without competing cache writes. - Independently triggered workflows remain isolated through their own cache namespaces. - Changes to the monitored Gradle configuration files produce a new cache key. - The normal Gradle/Spotless path is included when the shared cache is populated. ## Validation - Compared the cache key expressions and `hashFiles(...)` inputs across the affected workflows. - Verified that the central restore and save steps use the same precomputed key. - CI should confirm that the prime job populates the shared cache and downstream workflows only restore it. ## Checklist - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have performed a self-review of my changes - [ ] I have run the relevant CI checks - [ ] I have tested the workflow changes
618 lines
25 KiB
YAML
618 lines
25 KiB
YAML
name: PR Deployment via Comment
|
|
|
|
on:
|
|
issue_comment:
|
|
types: [created]
|
|
workflow_dispatch:
|
|
inputs:
|
|
pr_number:
|
|
description: "PR number to deploy"
|
|
required: true
|
|
enable_prototypes:
|
|
description: "Build with prototypes frontend"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
enable_pro:
|
|
description: "Enable pro features"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
enable_enterprise:
|
|
description: "Enable enterprise features"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
disable_security:
|
|
description: "Disable security/login"
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
check-comment:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read # actions/checkout
|
|
issues: write # add reaction to the triggering issue comment
|
|
if: |
|
|
vars.CI_PROFILE != 'lite' && (
|
|
github.event_name == 'workflow_dispatch' ||
|
|
(
|
|
github.event.issue.pull_request &&
|
|
(
|
|
contains(github.event.comment.body, 'prdeploy') ||
|
|
contains(github.event.comment.body, 'deploypr')
|
|
)
|
|
&&
|
|
(
|
|
github.event.comment.user.login == 'frooodle' ||
|
|
github.event.comment.user.login == 'sf298' ||
|
|
github.event.comment.user.login == 'Ludy87' ||
|
|
github.event.comment.user.login == 'balazs-szucs' ||
|
|
github.event.comment.user.login == 'reecebrowne' ||
|
|
github.event.comment.user.login == 'EthanHealy01' ||
|
|
github.event.comment.user.login == 'jbrunton96' ||
|
|
github.event.comment.user.login == 'ConnorYoh'
|
|
)
|
|
)
|
|
)
|
|
outputs:
|
|
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
|
comment_id: ${{ github.event.comment.id }}
|
|
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
|
|
enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }}
|
|
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
|
enable_prototypes: ${{ steps.check-prototypes-flag.outputs.enable_prototypes }}
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Checkout PR
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Get PR data
|
|
id: get-pr
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
const prNumber = context.eventName === 'workflow_dispatch'
|
|
? context.payload.inputs.pr_number
|
|
: context.payload.issue.number;
|
|
console.log(`PR Number: ${prNumber}`);
|
|
core.setOutput('pr_number', prNumber);
|
|
|
|
- name: Check for security/login flag
|
|
id: check-security-flag
|
|
env:
|
|
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
|
|
DISPATCH_DISABLE_SECURITY: ${{ inputs.disable_security }}
|
|
run: |
|
|
if [[ "$IS_DISPATCH" == "true" ]]; then
|
|
echo "disable_security=$DISPATCH_DISABLE_SECURITY" >> $GITHUB_OUTPUT
|
|
elif [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then
|
|
echo "disable_security=false" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "disable_security=true" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Check for pro flag
|
|
id: check-pro-flag
|
|
env:
|
|
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
|
|
DISPATCH_PRO: ${{ inputs.enable_pro }}
|
|
DISPATCH_ENTERPRISE: ${{ inputs.enable_enterprise }}
|
|
run: |
|
|
if [[ "$IS_DISPATCH" == "true" ]]; then
|
|
echo "enable_pro=$DISPATCH_PRO" >> $GITHUB_OUTPUT
|
|
echo "enable_enterprise=$DISPATCH_ENTERPRISE" >> $GITHUB_OUTPUT
|
|
elif [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then
|
|
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
|
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
|
elif [[ "$COMMENT_BODY" == *"enterprise"* ]]; then
|
|
echo "enable_enterprise=true" >> $GITHUB_OUTPUT
|
|
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "enable_pro=false" >> $GITHUB_OUTPUT
|
|
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Check for prototypes flag
|
|
id: check-prototypes-flag
|
|
env:
|
|
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
|
|
DISPATCH_PROTOTYPES: ${{ inputs.enable_prototypes }}
|
|
run: |
|
|
if [[ "$IS_DISPATCH" == "true" ]]; then
|
|
echo "enable_prototypes=$DISPATCH_PROTOTYPES" >> $GITHUB_OUTPUT
|
|
elif [[ "$COMMENT_BODY" == *"prototypes"* ]]; then
|
|
echo "Prototypes flag detected in comment"
|
|
echo "enable_prototypes=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "No prototypes flag detected in comment"
|
|
echo "enable_prototypes=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Add 'in_progress' reaction to comment
|
|
if: github.event_name == 'issue_comment'
|
|
id: add-eyes-reaction
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
console.log(`Adding eyes reaction to comment ID: ${context.payload.comment.id}`);
|
|
try {
|
|
const { data: reaction } = await github.rest.reactions.createForIssueComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: context.payload.comment.id,
|
|
content: 'eyes'
|
|
});
|
|
console.log(`Added reaction with ID: ${reaction.id}`);
|
|
return { success: true, id: reaction.id };
|
|
} catch (error) {
|
|
console.error(`Failed to add reaction: ${error.message}`);
|
|
console.error(error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
|
|
deploy-pr:
|
|
environment: pr-preview
|
|
needs: check-comment
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read # actions/checkout, incl. the PR merge ref
|
|
issues: write # reactions, 'pr-deployed' label, deployment URL comment
|
|
pull-requests: write
|
|
packages: write # push PR image to ghcr.io
|
|
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Checkout PR
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Checkout PR
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
|
# untrusted tree gets built below - never leave credentials in .git/config
|
|
persist-credentials: false
|
|
|
|
- name: Cache Gradle
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-deploy-pr-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
|
|
|
- name: Set up JDK 25
|
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: "temurin"
|
|
|
|
- name: Install Task
|
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
|
- name: Run Gradle Command
|
|
run: |
|
|
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
|
|
export DISABLE_ADDITIONAL_FEATURES=true
|
|
else
|
|
export DISABLE_ADDITIONAL_FEATURES=false
|
|
fi
|
|
task backend:build
|
|
env:
|
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
|
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
|
STIRLING_PDF_DESKTOP_UI: false
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ github.token }}
|
|
|
|
- name: Convert repository owner to lowercase
|
|
id: repoowner
|
|
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
|
|
|
- name: Build and push PR-specific image
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
with:
|
|
context: .
|
|
file: ./docker/embedded/Dockerfile
|
|
push: true
|
|
cache-from: type=gha,scope=stirling-pdf-latest
|
|
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
|
|
tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:pr-${{ needs.check-comment.outputs.pr_number }}
|
|
build-args: |
|
|
VERSION_TAG=alpha
|
|
PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }}
|
|
platforms: linux/amd64
|
|
|
|
- name: Build and push engine image
|
|
if: needs.check-comment.outputs.enable_prototypes == 'true'
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
with:
|
|
context: .
|
|
file: ./engine/Dockerfile
|
|
push: true
|
|
cache-from: type=gha,scope=stirling-pdf-engine
|
|
cache-to: type=gha,mode=max,scope=stirling-pdf-engine
|
|
tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:engine-pr-${{ needs.check-comment.outputs.pr_number }}
|
|
platforms: linux/amd64
|
|
|
|
- name: Set up SSH
|
|
run: |
|
|
mkdir -p ~/.ssh/
|
|
echo "${NEW_VPS_SSH_KEY}" > ../private.key
|
|
sudo chmod 600 ../private.key
|
|
|
|
env:
|
|
NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
|
|
- name: Deploy to VPS
|
|
id: deploy
|
|
run: |
|
|
# Set security settings based on flags
|
|
if [ "${{ needs.check-comment.outputs.disable_security }}" == "false" ]; then
|
|
DISABLE_ADDITIONAL_FEATURES="false"
|
|
LOGIN_SECURITY="true"
|
|
SECURITY_STATUS="🔒 Security Enabled"
|
|
else
|
|
DISABLE_ADDITIONAL_FEATURES="true"
|
|
LOGIN_SECURITY="false"
|
|
SECURITY_STATUS="Security Disabled"
|
|
fi
|
|
|
|
# Set pro/enterprise settings (enterprise implies pro)
|
|
if [ "${{ needs.check-comment.outputs.enable_enterprise }}" == "true" ]; then
|
|
PREMIUM_ENABLED="true"
|
|
PREMIUM_KEY="${ENTERPRISE_KEY}"
|
|
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
|
elif [ "${{ needs.check-comment.outputs.enable_pro }}" == "true" ]; then
|
|
PREMIUM_ENABLED="true"
|
|
PREMIUM_KEY="${PRO_KEY}"
|
|
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
|
else
|
|
PREMIUM_ENABLED="false"
|
|
PREMIUM_KEY=""
|
|
PREMIUM_PROFEATURES_AUDIT_ENABLED="false"
|
|
fi
|
|
|
|
ENABLE_PROTOTYPES="${{ needs.check-comment.outputs.enable_prototypes }}"
|
|
PR_NUMBER="${{ needs.check-comment.outputs.pr_number }}"
|
|
|
|
# Build engine env vars for backend (only set when prototypes enabled)
|
|
if [ "$ENABLE_PROTOTYPES" == "true" ]; then
|
|
AI_ENGINE_VARS="
|
|
SYSTEM_AIENGINE_ENABLED: \"true\"
|
|
SYSTEM_AIENGINE_URL: \"http://stirling-pdf-engine-pr-${PR_NUMBER}:5001\""
|
|
ENGINE_SERVICE="
|
|
stirling-pdf-engine:
|
|
container_name: stirling-pdf-engine-pr-${PR_NUMBER}
|
|
image: ${IMAGE_BASE}:engine-pr-${PR_NUMBER}
|
|
environment:
|
|
ANTHROPIC_API_KEY: \"${ANTHROPIC_API_KEY}\"
|
|
networks:
|
|
- pr-network
|
|
restart: on-failure:5"
|
|
NETWORK_SECTION="
|
|
networks:
|
|
pr-network:"
|
|
BACKEND_NETWORK="
|
|
networks:
|
|
- pr-network"
|
|
else
|
|
AI_ENGINE_VARS=""
|
|
ENGINE_SERVICE=""
|
|
NETWORK_SECTION=""
|
|
BACKEND_NETWORK=""
|
|
fi
|
|
|
|
# First create the docker-compose content locally
|
|
cat > docker-compose.yml << EOF
|
|
version: '3.3'
|
|
services:
|
|
stirling-pdf:
|
|
container_name: stirling-pdf-pr-${PR_NUMBER}
|
|
image: ${IMAGE_BASE}:pr-${PR_NUMBER}
|
|
ports:
|
|
- "${PR_NUMBER}:8080"
|
|
volumes:
|
|
- /stirling/PR-${PR_NUMBER}/data:/usr/share/tessdata:rw
|
|
- /stirling/PR-${PR_NUMBER}/config:/configs:rw
|
|
- /stirling/PR-${PR_NUMBER}/logs:/logs:rw
|
|
environment:
|
|
DISABLE_ADDITIONAL_FEATURES: "${DISABLE_ADDITIONAL_FEATURES}"
|
|
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
|
|
SYSTEM_DEFAULTLOCALE: en-US
|
|
UI_APPNAME: "Stirling-PDF PR#${PR_NUMBER}"
|
|
UI_HOMEDESCRIPTION: "PR#${PR_NUMBER} for Stirling-PDF Latest"
|
|
UI_APPNAMENAVBAR: "PR#${PR_NUMBER}"
|
|
SYSTEM_MAXFILESIZE: "100"
|
|
METRICS_ENABLED: "true"
|
|
SYSTEM_GOOGLEVISIBILITY: "false"
|
|
PREMIUM_KEY: "${PREMIUM_KEY}"
|
|
PREMIUM_ENABLED: "${PREMIUM_ENABLED}"
|
|
PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}"${AI_ENGINE_VARS}
|
|
restart: on-failure:5${BACKEND_NETWORK}${ENGINE_SERVICE}${NETWORK_SECTION}
|
|
EOF
|
|
|
|
# Then copy the file and execute commands
|
|
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose.yml
|
|
|
|
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH
|
|
# Create PR-specific directories
|
|
mkdir -p /stirling/PR-${PR_NUMBER}/{data,config,logs}
|
|
|
|
# Move docker-compose file to correct location
|
|
mv /tmp/docker-compose.yml /stirling/PR-${PR_NUMBER}/docker-compose.yml
|
|
|
|
# Start or restart the container
|
|
cd /stirling/PR-${PR_NUMBER}
|
|
docker-compose pull
|
|
docker-compose up -d
|
|
ENDSSH
|
|
|
|
# Set output for use in PR comment
|
|
echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV
|
|
|
|
env:
|
|
ENTERPRISE_KEY: ${{ secrets.ENTERPRISE_KEY }}
|
|
# named PRO_KEY, not PREMIUM_KEY, so the shell var it feeds is not self-referential
|
|
PRO_KEY: ${{ secrets.PREMIUM_KEY }}
|
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
|
|
NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
|
|
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
|
- name: Add success reaction to comment
|
|
if: success() && github.event_name == 'issue_comment'
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
console.log(`Adding rocket reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
|
|
try {
|
|
const { data: reaction } = await github.rest.reactions.createForIssueComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: ${{ needs.check-comment.outputs.comment_id }},
|
|
content: 'rocket'
|
|
});
|
|
console.log(`Added rocket reaction with ID: ${reaction.id}`);
|
|
} catch (error) {
|
|
console.error(`Failed to add reaction: ${error.message}`);
|
|
console.error(error);
|
|
}
|
|
|
|
// add label to PR
|
|
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
|
try {
|
|
await github.rest.issues.addLabels({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
labels: ['pr-deployed']
|
|
});
|
|
console.log(`Added 'pr-deployed' label to PR #${prNumber}`);
|
|
} catch (error) {
|
|
console.error(`Failed to add label to PR: ${error.message}`);
|
|
console.error(error);
|
|
}
|
|
|
|
- name: Add failure reaction to comment
|
|
if: failure() && github.event_name == 'issue_comment'
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
console.log(`Adding -1 reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
|
|
try {
|
|
const { data: reaction } = await github.rest.reactions.createForIssueComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: ${{ needs.check-comment.outputs.comment_id }},
|
|
content: '-1'
|
|
});
|
|
console.log(`Added -1 reaction with ID: ${reaction.id}`);
|
|
} catch (error) {
|
|
console.error(`Failed to add reaction: ${error.message}`);
|
|
console.error(error);
|
|
}
|
|
|
|
- name: Post deployment URL to PR
|
|
if: success()
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
env:
|
|
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
const { GITHUB_REPOSITORY } = process.env;
|
|
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
|
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
|
const securityStatus = process.env.security_status || "Security Disabled";
|
|
|
|
const deploymentUrl = `http://${process.env.NEW_VPS_HOST}:${prNumber}`;
|
|
const commentBody = `## 🚀 PR Test Deployment\n\n` +
|
|
`Your PR has been deployed for testing!\n\n` +
|
|
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` +
|
|
`${securityStatus}\n\n` +
|
|
`This deployment will be automatically cleaned up when the PR is closed.\n\n`;
|
|
|
|
await github.rest.issues.createComment({
|
|
owner: repoOwner,
|
|
repo: repoName,
|
|
issue_number: prNumber,
|
|
body: commentBody
|
|
});
|
|
|
|
- name: Cleanup temporary files
|
|
if: always()
|
|
run: |
|
|
echo "Cleaning up temporary files..."
|
|
rm -f ../private.key docker-compose.yml
|
|
echo "Cleanup complete."
|
|
continue-on-error: true
|
|
|
|
handle-label-commands:
|
|
if: ${{ github.event.issue.pull_request != null }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read # actions/checkout, reads repo_devs.json and labels.yml
|
|
issues: write # add/remove labels, delete the command comment
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Check out the repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Apply label commands
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const { comment, issue } = context.payload;
|
|
const commentBody = comment?.body ?? '';
|
|
if (!commentBody.includes('::label::')) {
|
|
core.info('No label commands detected in comment.');
|
|
return;
|
|
}
|
|
|
|
const configPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'config', 'repo_devs.json');
|
|
const repoDevsConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
const label_changer = (repoDevsConfig.label_changer || []).map((login) => login.toLowerCase());
|
|
|
|
const commenter = (comment?.user?.login || '').toLowerCase();
|
|
if (!label_changer.includes(commenter)) {
|
|
core.info(`User ${commenter} is not authorized to manage labels.`);
|
|
return;
|
|
}
|
|
|
|
const labelsConfigPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'labels.yml');
|
|
const labelsFile = fs.readFileSync(labelsConfigPath, 'utf8');
|
|
|
|
const labelNameMap = new Map();
|
|
for (const match of labelsFile.matchAll(/-\s+name:\s*(?:"([^"]+)"|'([^']+)'|([^\n]+))/g)) {
|
|
const labelName = (match[1] ?? match[2] ?? match[3] ?? '').trim();
|
|
|
|
if (!labelName) {
|
|
continue;
|
|
}
|
|
const normalized = labelName.toLowerCase();
|
|
if (!labelNameMap.has(normalized)) {
|
|
labelNameMap.set(normalized, labelName);
|
|
}
|
|
}
|
|
|
|
if (!labelNameMap.size) {
|
|
core.warning('No labels could be read from .github/labels.yml; aborting label commands.');
|
|
return;
|
|
}
|
|
|
|
let allowedLabelNames = new Set(labelNameMap.values());
|
|
|
|
const labelsToAdd = new Set();
|
|
const labelsToRemove = new Set();
|
|
const commandRegex = /^(\w+)::(label)::"([^"]+)"/gim;
|
|
let match;
|
|
while ((match = commandRegex.exec(commentBody)) !== null) {
|
|
core.info(`Found label command: ${match[0]} (action: ${match[1]}, label: ${match[2]}, labelName: ${match[3]})`);
|
|
const action = match[1].toLowerCase();
|
|
const labelName = match[3].trim();
|
|
|
|
if (!labelName) {
|
|
continue;
|
|
}
|
|
|
|
const normalized = labelName.toLowerCase();
|
|
const resolvedLabelName = labelNameMap.get(normalized);
|
|
if (action === 'add') {
|
|
if (!resolvedLabelName) {
|
|
core.warning(`Label "${labelName}" is not defined in .github/labels.yml and cannot be added.`);
|
|
continue;
|
|
}
|
|
if (!allowedLabelNames.has(resolvedLabelName)) {
|
|
core.warning(`Label "${resolvedLabelName}" is not allowed for add commands and will be skipped.`);
|
|
continue;
|
|
}
|
|
labelsToAdd.add(resolvedLabelName);
|
|
} else if (action === 'rm') {
|
|
const labelToRemove = resolvedLabelName ?? labelName;
|
|
if (!resolvedLabelName) {
|
|
core.warning(`Label "${labelName}" is not defined in .github/labels.yml; attempting to remove as provided.`);
|
|
}
|
|
labelsToRemove.add(labelToRemove);
|
|
}
|
|
}
|
|
|
|
const addLabels = Array.from(labelsToAdd);
|
|
const removeLabels = Array.from(labelsToRemove);
|
|
|
|
if (!addLabels.length && !removeLabels.length) {
|
|
core.info('No valid label commands found after parsing.');
|
|
return;
|
|
}
|
|
|
|
const issueParams = {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issue.number,
|
|
};
|
|
|
|
if (addLabels.length) {
|
|
core.info(`Adding labels: ${addLabels.join(', ')}`);
|
|
await github.rest.issues.addLabels({
|
|
...issueParams,
|
|
labels: addLabels,
|
|
});
|
|
}
|
|
|
|
for (const labelName of removeLabels) {
|
|
core.info(`Removing label: ${labelName}`);
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
...issueParams,
|
|
name: labelName,
|
|
});
|
|
} catch (error) {
|
|
if (error.status === 404) {
|
|
core.warning(`Label "${labelName}" was not present on the pull request.`);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
await github.rest.issues.deleteComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: comment.id,
|
|
});
|
|
core.info('Processed label commands and deleted the comment.');
|