From 0223975bbdbcb3a5e2bb2c538c844195fbace4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Mon, 2 Mar 2026 10:57:14 +0100 Subject: [PATCH] Harden GitHub Actions: fix injections, isolate privileged operations to ci-privileged repo (#18318) ## Summary - Fix expression injection vulnerabilities in composite actions (`restore-cache`, `nx-affected`) and workflow files (`claude.yml`) - Reduce overly broad permissions in `ci-utils.yaml` (Danger.js) and `ci-breaking-changes.yaml` - Restructure `preview-env-dispatch.yaml`: auto-trigger for members, opt-in for contributor PRs via `preview-app` label (safe because keepalive has no write tokens) - Isolate all write-access operations (PR comments, cross-repo posting) to a new dedicated [`twentyhq/ci-privileged`](https://github.com/twentyhq/ci-privileged) repo via `repository_dispatch`, so that workflows in twenty that execute contributor code never have write tokens - Create `post-ci-comments.yaml` (`workflow_run` bridge) to dispatch breaking changes results to ci-privileged, solving the [fork PR comment issue](https://github.com/twentyhq/twenty/pull/13713#issuecomment-3168999083) - Delete 5 unused secrets and broken `i18n-qa-report` workflow - Remove `TWENTY_DISPATCH_TOKEN` from twenty (moved to ci-privileged as `CORE_TEAM_ISSUES_COMMENT_TOKEN`) - Use `toJSON()` for all `client-payload` values to prevent JSON injection ## Security model after this PR | Workflow | Executes fork code? | Write tokens available? | |----------|---------------------|------------------------| | preview-env-keepalive | Yes | None (contents: read only) | | preview-env-dispatch | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN only | | ci-breaking-changes | Yes | None (contents: read only) | | post-ci-comments (workflow_run) | No (default branch) | CI_PRIVILEGED_DISPATCH_TOKEN only | | claude.yml | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN | | ci-utils (Danger.js) | No (base branch) | GITHUB_TOKEN (scoped) | All actual write tokens (`TWENTY_PR_COMMENT_TOKEN`, `CORE_TEAM_ISSUES_COMMENT_TOKEN`) live in `twentyhq/ci-privileged` with strict CODEOWNERS review and branch protection. ## Test plan - [ ] Verify preview environment comments still appear on member PRs - [ ] Verify adding `preview-app` label triggers preview for contributor PRs - [ ] Verify breaking changes reports still post on PRs (including fork PRs) - [ ] Verify Claude cross-repo responses still post on core-team-issues - [ ] Confirm ci-privileged branch protection is enforced --- .github/actions/nx-affected/action.yaml | 8 +- .github/actions/restore-cache/action.yaml | 5 +- .github/workflows/ci-breaking-changes.yaml | 196 +------------------ .github/workflows/ci-utils.yaml | 3 - .github/workflows/claude.yml | 24 +-- .github/workflows/i18n-qa-report.yaml | 118 ----------- .github/workflows/post-ci-comments.yaml | 71 +++++++ .github/workflows/preview-env-dispatch.yaml | 28 ++- .github/workflows/preview-env-keepalive.yaml | 66 ++----- 9 files changed, 138 insertions(+), 381 deletions(-) delete mode 100644 .github/workflows/i18n-qa-report.yaml create mode 100644 .github/workflows/post-ci-comments.yaml diff --git a/.github/actions/nx-affected/action.yaml b/.github/actions/nx-affected/action.yaml index dfb66443063..4ffa39d5bb1 100644 --- a/.github/actions/nx-affected/action.yaml +++ b/.github/actions/nx-affected/action.yaml @@ -19,4 +19,10 @@ runs: uses: nrwl/nx-set-shas@v4 - name: Run affected command shell: bash - run: npx nx affected --nxBail --configuration=${{ inputs.configuration }} -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}' ${{ inputs.args }} \ No newline at end of file + env: + NX_CONFIGURATION: ${{ inputs.configuration }} + NX_TASKS: ${{ inputs.tasks }} + NX_PARALLEL: ${{ inputs.parallel }} + NX_TAG: ${{ inputs.tag }} + NX_ARGS: ${{ inputs.args }} + run: npx nx affected --nxBail --configuration="$NX_CONFIGURATION" -t="$NX_TASKS" --parallel="$NX_PARALLEL" --exclude="*,!tag:$NX_TAG" $NX_ARGS \ No newline at end of file diff --git a/.github/actions/restore-cache/action.yaml b/.github/actions/restore-cache/action.yaml index 9c8ee524480..16955dd1407 100644 --- a/.github/actions/restore-cache/action.yaml +++ b/.github/actions/restore-cache/action.yaml @@ -19,8 +19,11 @@ runs: - name: Cache primary key builder id: cache-primary-key-builder shell: bash + env: + CACHE_KEY: ${{ inputs.key }} + REF_NAME: ${{ github.ref_name }} run: | - echo "CACHE_PRIMARY_KEY_PREFIX=v4-${{ inputs.key }}-${{ github.ref_name }}" >> "${GITHUB_OUTPUT}" + echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}" - name: Restore cache uses: actions/cache/restore@v4 id: restore-cache diff --git a/.github/workflows/ci-breaking-changes.yaml b/.github/workflows/ci-breaking-changes.yaml index f745f155857..c5435f90adb 100644 --- a/.github/workflows/ci-breaking-changes.yaml +++ b/.github/workflows/ci-breaking-changes.yaml @@ -16,8 +16,6 @@ env: permissions: contents: read - pull-requests: write - checks: write jobs: changed-files-check: @@ -584,182 +582,16 @@ jobs: echo "::warning::REST Metadata API analysis tool error - continuing workflow" fi - - name: Comment API Changes on PR + - name: Upload breaking changes report if: always() - uses: actions/github-script@v7 + uses: actions/upload-artifact@v4 with: - script: | - const fs = require('fs'); - let hasChanges = false; - let comment = ''; - - try { - if (fs.existsSync('graphql-schema-diff.md')) { - const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8'); - if (graphqlDiff.trim()) { - if (!hasChanges) { - comment = '## šŸ“Š API Changes Report\n\n'; - hasChanges = true; - } - comment += '### GraphQL Schema Changes\n' + graphqlDiff + '\n\n'; - } - } - - if (fs.existsSync('graphql-metadata-diff.md')) { - const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8'); - if (graphqlMetadataDiff.trim()) { - if (!hasChanges) { - comment = '## šŸ“Š API Changes Report\n\n'; - hasChanges = true; - } - comment += '### GraphQL Metadata Schema Changes\n' + graphqlMetadataDiff + '\n\n'; - } - } - - if (fs.existsSync('rest-api-diff.md')) { - const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8'); - if (restDiff.trim()) { - if (!hasChanges) { - comment = '## šŸ“Š API Changes Report\n\n'; - hasChanges = true; - } - comment += restDiff + '\n\n'; - } - } - - if (fs.existsSync('rest-metadata-api-diff.md')) { - const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8'); - if (metadataDiff.trim()) { - if (!hasChanges) { - comment = '## šŸ“Š API Changes Report\n\n'; - hasChanges = true; - } - comment += metadataDiff + '\n\n'; - } - } - - // Only post comment if there are changes - if (hasChanges) { - // Add branch state information only if there were conflicts - const branchState = process.env.BRANCH_STATE || 'unknown'; - let branchStateNote = ''; - - if (branchState === 'conflicts') { - branchStateNote = '\n\nāš ļø **Note**: Could not merge with `main` due to conflicts. This comparison shows changes between the current branch and `main` as separate states.\n'; - } - // Check if there are any breaking changes detected - let hasBreakingChanges = false; - let breakingChangeNote = ''; - - // Check for breaking changes in any of the diff files - if (fs.existsSync('rest-api-diff.md')) { - const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8'); - if (restDiff.includes('Breaking Changes') || restDiff.includes('🚨') || - restDiff.includes('Removed Endpoints') || restDiff.includes('Changed Operations')) { - hasBreakingChanges = true; - } - } - - if (fs.existsSync('rest-metadata-api-diff.md')) { - const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8'); - if (metadataDiff.includes('Breaking Changes') || metadataDiff.includes('🚨') || - metadataDiff.includes('Removed Endpoints') || metadataDiff.includes('Changed Operations')) { - hasBreakingChanges = true; - } - } - - // Also check GraphQL changes for breaking changes indicators - if (fs.existsSync('graphql-schema-diff.md')) { - const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8'); - if (graphqlDiff.includes('Breaking changes') || graphqlDiff.includes('BREAKING')) { - hasBreakingChanges = true; - } - } - - if (fs.existsSync('graphql-metadata-diff.md')) { - const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8'); - if (graphqlMetadataDiff.includes('Breaking changes') || graphqlMetadataDiff.includes('BREAKING')) { - hasBreakingChanges = true; - } - } - - // Check PR title for "breaking" - const prTitle = ${{ toJSON(github.event.pull_request.title) }}; - const titleContainsBreaking = prTitle.toLowerCase().includes('breaking'); - - if (hasBreakingChanges) { - if (titleContainsBreaking) { - breakingChangeNote = '\n\n## āœ… Breaking Change Protocol\n\n' + - '**This PR title contains "breaking" and breaking changes were detected - the CI will fail as expected.**\n\n' + - 'šŸ“ **Action Required**: Please add `BREAKING CHANGE:` to your commit message to trigger a major version bump.\n\n' + - 'Example:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```'; - } else { - breakingChangeNote = '\n\n## āš ļø Breaking Change Protocol\n\n' + - '**Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.**\n\n' + - 'šŸ”„ **Options**:\n' + - '1. **If this IS a breaking change**: Add "breaking" to your PR title and add `BREAKING CHANGE:` to your commit message\n' + - '2. **If this is NOT a breaking change**: The API diff tool may have false positives - please review carefully\n\n' + - 'For breaking changes, add to commit message:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```'; - } - } - - const COMMENT_MARKER = ''; - const commentBody = COMMENT_MARKER + '\n' + comment + branchStateNote + '\nāš ļø **Please review these API changes carefully before merging.**' + breakingChangeNote; - - // Get all comments to find existing API changes comment - const {data: comments} = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - // Find our existing comment - const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER)); - - if (botComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: commentBody - }); - console.log('Updated existing API changes comment'); - } else { - // Create new comment - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: commentBody - }); - console.log('Created new API changes comment'); - } - } else { - console.log('No API changes detected - skipping PR comment'); - - // Check if there's an existing comment to remove - const {data: comments} = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const COMMENT_MARKER = ''; - const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER)); - - if (botComment) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - }); - console.log('Deleted existing API changes comment (no changes detected)'); - } - } - } catch (error) { - console.log('Could not post comment:', error); - } + name: breaking-changes-report + path: | + *-diff.md + *-diff.json + if-no-files-found: ignore + retention-days: 3 - name: Cleanup servers if: always() @@ -771,16 +603,4 @@ jobs: kill $(cat /tmp/main-server.pid) || true fi - # - name: Upload API specifications and diffs - # if: always() - # uses: actions/upload-artifact@v4 - # with: - # name: api-specifications-and-diffs - # path: | - # /tmp/main-server.log - # /tmp/current-server.log - # *-api.json - # *-schema-introspection.json - # *-diff.md - # *-diff.json diff --git a/.github/workflows/ci-utils.yaml b/.github/workflows/ci-utils.yaml index a7c08a1adc9..0803e3a2836 100644 --- a/.github/workflows/ci-utils.yaml +++ b/.github/workflows/ci-utils.yaml @@ -9,10 +9,7 @@ on: types: [opened, synchronize, reopened, closed] permissions: - actions: write checks: write - contents: write - issues: write pull-requests: write statuses: write diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 37747d40839..86b22571ded 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -87,7 +87,7 @@ jobs: exit 0 fi ISSUE_NUMBER="${{ github.event.issue.number || github.event.pull_request.number }}" - ENCODED_BRANCH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$BRANCH', safe=''))") + ENCODED_BRANCH=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$BRANCH") PR_URL="https://github.com/${{ github.repository }}/compare/main...${ENCODED_BRANCH}?quick_pull=1" BODY="āš ļø Claude ran out of turns before creating a PR. Work has been pushed to [\`$BRANCH\`](https://github.com/${{ github.repository }}/tree/$ENCODED_BRANCH).\n\n[**Create PR →**]($PR_URL)" if [ -n "$ISSUE_NUMBER" ]; then @@ -157,21 +157,11 @@ jobs: "PG_DATABASE_URL": "postgres://postgres:postgres@localhost:5432/default" } } - - name: Post response to source issue + - name: Dispatch response to ci-privileged if: always() - uses: actions/github-script@v7 - env: - TARGET_REPO: ${{ steps.prompt.outputs.repo }} - TARGET_ISSUE: ${{ steps.prompt.outputs.issue_number }} + uses: peter-evans/repository-dispatch@v2 with: - github-token: ${{ secrets.TWENTY_DISPATCH_TOKEN }} - script: | - const [owner, repo] = process.env.TARGET_REPO.split('/'); - const issueNumber = parseInt(process.env.TARGET_ISSUE, 10); - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: issueNumber, - body: `Claude finished processing this request. [See workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})` - }); + token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} + repository: twentyhq/ci-privileged + event-type: claude-cross-repo-response + client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": ${{ toJSON(format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id)) }}}' diff --git a/.github/workflows/i18n-qa-report.yaml b/.github/workflows/i18n-qa-report.yaml deleted file mode 100644 index a25bef02b54..00000000000 --- a/.github/workflows/i18n-qa-report.yaml +++ /dev/null @@ -1,118 +0,0 @@ -# Weekly translation QA report using Crowdin's native QA checks - -name: 'Weekly Translation QA Report' - -permissions: - contents: write - pull-requests: write - -on: - schedule: - - cron: '0 9 * * 1' # Every Monday at 9am UTC - workflow_dispatch: # Allow manual trigger - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - qa_report: - name: Generate QA Report - runs-on: depot-ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install dependencies - uses: ./.github/actions/yarn-install - - - name: Build twenty-shared - run: npx nx build twenty-shared - - - name: Generate QA report from Crowdin - id: generate_report - run: | - npx ts-node packages/twenty-utils/translation-qa-report.ts || true - if [ -f TRANSLATION_QA_REPORT.md ]; then - echo "report_generated=true" >> $GITHUB_OUTPUT - # Count critical issues (exclude spellcheck) - CRITICAL=$(grep -oP 'āš ļø\s+\K\d+' TRANSLATION_QA_REPORT.md 2>/dev/null || echo "0") - echo "critical_issues=$CRITICAL" >> $GITHUB_OUTPUT - else - echo "report_generated=false" >> $GITHUB_OUTPUT - echo "critical_issues=0" >> $GITHUB_OUTPUT - fi - env: - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - - name: Create QA branch and commit report - if: steps.generate_report.outputs.report_generated == 'true' - run: | - git config --global user.name 'github-actions' - git config --global user.email 'github-actions@twenty.com' - - BRANCH_NAME="i18n-qa-report-$(date +%Y-%m-%d)" - git checkout -B $BRANCH_NAME - - git add TRANSLATION_QA_REPORT.md - if ! git diff --staged --quiet --exit-code; then - git commit -m "docs: weekly translation QA report" - git push origin HEAD:$BRANCH_NAME --force - echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV - else - echo "No changes to commit" - echo "BRANCH_NAME=" >> $GITHUB_ENV - fi - - - name: Create pull request - if: steps.generate_report.outputs.report_generated == 'true' && env.BRANCH_NAME != '' - run: | - CRITICAL="${{ steps.generate_report.outputs.critical_issues }}" - - BODY=$(cat < "Fix the translation QA issues using the Crowdin API" - - The AI can help fix: - - āœ… Variables mismatch (missing/wrong placeholders) - - āœ… Escaped Unicode sequences - - āš ļø Tags mismatch - - āš ļø Empty translations - - ### Available Scripts - - \`\`\`bash - # View QA report - CROWDIN_PERSONAL_TOKEN=xxx npx ts-node packages/twenty-utils/translation-qa-report.ts - - # Fix encoding issues automatically - CROWDIN_PERSONAL_TOKEN=xxx npx ts-node packages/twenty-utils/fix-crowdin-translations.ts - \`\`\` - - --- - *Close without merging after issues are addressed* - EOF - ) - - EXISTING_PR=$(gh pr list --head $BRANCH_NAME --json number --jq '.[0].number' 2>/dev/null || echo "") - - if [ -n "$EXISTING_PR" ]; then - gh pr edit $EXISTING_PR --body "$BODY" - else - gh pr create \ - --base main \ - --head $BRANCH_NAME \ - --title "i18n: Translation QA Report ($CRITICAL critical issues)" \ - --body "$BODY" || true - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/post-ci-comments.yaml b/.github/workflows/post-ci-comments.yaml new file mode 100644 index 00000000000..4912cd4a691 --- /dev/null +++ b/.github/workflows/post-ci-comments.yaml @@ -0,0 +1,71 @@ +name: Post CI Comments + +on: + workflow_run: + workflows: ['GraphQL and OpenAPI Breaking Changes Detection'] + types: [completed] + +permissions: + actions: read + +jobs: + dispatch-breaking-changes: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Get PR number from workflow run + id: pr-info + uses: actions/github-script@v7 + with: + script: | + const runId = context.payload.workflow_run.id; + const headSha = context.payload.workflow_run.head_sha; + const headBranch = context.payload.workflow_run.head_branch; + const headRepo = context.payload.workflow_run.head_repository; + + // workflow_run.pull_requests is empty for fork PRs, + // so fall back to searching by head SHA + let pullRequests = context.payload.workflow_run.pull_requests; + let prNumber; + + if (pullRequests && pullRequests.length > 0) { + prNumber = pullRequests[0].number; + } else { + core.info(`pull_requests is empty (likely a fork PR), searching by SHA ${headSha}`); + const owner = context.repo.owner; + const repo = context.repo.repo; + const headLabel = `${headRepo.owner.login}:${headBranch}`; + + const { data: prs } = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + head: headLabel, + per_page: 1, + }); + + if (prs.length > 0) { + prNumber = prs[0].number; + } + } + + if (!prNumber) { + core.info('No pull request found for this workflow run'); + core.setOutput('has_pr', 'false'); + return; + } + + core.setOutput('pr_number', prNumber); + core.setOutput('run_id', runId); + core.setOutput('has_pr', 'true'); + core.info(`PR #${prNumber}, Run ID: ${runId}`); + + - name: Dispatch to ci-privileged + if: steps.pr-info.outputs.has_pr == 'true' + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} + repository: twentyhq/ci-privileged + event-type: breaking-changes-report + client-payload: '{"pr_number": ${{ toJSON(steps.pr-info.outputs.pr_number) }}, "run_id": ${{ toJSON(steps.pr-info.outputs.run_id) }}, "repo": ${{ toJSON(github.repository) }}, "branch_state": ${{ toJSON(github.event.workflow_run.head_branch) }}}' diff --git a/.github/workflows/preview-env-dispatch.yaml b/.github/workflows/preview-env-dispatch.yaml index 54eca3a8a8a..c049e98127d 100644 --- a/.github/workflows/preview-env-dispatch.yaml +++ b/.github/workflows/preview-env-dispatch.yaml @@ -1,14 +1,10 @@ name: 'Preview Environment Dispatch' permissions: - contents: write + contents: read actions: write - pull-requests: read on: - # Using pull_request_target instead of pull_request to have access to secrets for external contributors - # Security note: This is safe because we're only using the repository-dispatch action with limited scope - # and not checking out or running any code from the external contributor's PR pull_request_target: types: [opened, synchronize, reopened, labeled] paths: @@ -24,7 +20,19 @@ concurrency: jobs: trigger-preview: - if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || (github.event.action == 'labeled' && github.event.label.name == 'preview-app') + if: | + (github.event.action == 'labeled' && github.event.label.name == 'preview-app') || + ( + ( + github.event.pull_request.author_association == 'MEMBER' || + github.event.pull_request.author_association == 'OWNER' || + github.event.pull_request.author_association == 'COLLABORATOR' + ) && ( + github.event.action == 'opened' || + github.event.action == 'synchronize' || + github.event.action == 'reopened' + ) + ) timeout-minutes: 5 runs-on: depot-ubuntu-24.04 steps: @@ -35,3 +43,11 @@ jobs: repository: ${{ github.repository }} event-type: preview-environment client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}' + + - name: Dispatch to ci-privileged for PR comment + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} + repository: twentyhq/ci-privileged + event-type: preview-env-url + client-payload: '{"pr_number": ${{ toJSON(github.event.pull_request.number) }}, "keepalive_dispatch_time": ${{ toJSON(github.event.pull_request.updated_at) }}, "repo": ${{ toJSON(github.repository) }}}' diff --git a/.github/workflows/preview-env-keepalive.yaml b/.github/workflows/preview-env-keepalive.yaml index be7224cd669..93d8bdffdf8 100644 --- a/.github/workflows/preview-env-keepalive.yaml +++ b/.github/workflows/preview-env-keepalive.yaml @@ -2,7 +2,6 @@ name: 'Preview Environment Keep Alive' permissions: contents: read - pull-requests: write on: repository_dispatch: @@ -55,14 +54,15 @@ jobs: port: 3000 - name: Start services with correct SERVER_URL + env: + TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }} run: | cd packages/twenty-docker/ - # Update the SERVER_URL with the tunnel URL - echo "Setting SERVER_URL to ${{ steps.expose-tunnel.outputs.tunnel-url }}" + echo "Setting SERVER_URL to $TUNNEL_URL" sed -i '/SERVER_URL=/d' .env echo "" >> .env - echo "SERVER_URL=${{ steps.expose-tunnel.outputs.tunnel-url }}" >> .env + echo "SERVER_URL=$TUNNEL_URL" >> .env # Start the services echo "Docker compose up..." @@ -99,54 +99,26 @@ jobs: fi working-directory: ./ - - name: Output tunnel URL to logs + - name: Output tunnel URL + env: + TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }} run: | echo "āœ… Preview Environment Ready!" - echo "šŸ”— Preview URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}" + echo "šŸ”— Preview URL: $TUNNEL_URL" echo "ā±ļø This environment will be available for 5 hours" + echo "## šŸš€ Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY" + echo "$TUNNEL_URL" > tunnel-url.txt - - name: Post comment on PR - uses: actions/github-script@v6 + - name: Upload tunnel URL artifact + uses: actions/upload-artifact@v4 with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const COMMENT_MARKER = ''; - const commentBody = `${COMMENT_MARKER} - šŸš€ **Preview Environment Ready!** - - Your preview environment is available at: ${{ steps.expose-tunnel.outputs.tunnel-url }} - - This environment will automatically shut down when the PR is closed or after 5 hours.`; - - // Get all comments - const {data: comments} = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: ${{ github.event.client_payload.pr_number }}, - }); - - // Find our comment - const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER)); - - if (botComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: commentBody - }); - console.log('Updated existing comment'); - } else { - // Create new comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: ${{ github.event.client_payload.pr_number }}, - body: commentBody - }); - console.log('Created new comment'); - } + name: tunnel-url + path: tunnel-url.txt + retention-days: 1 - name: Keep tunnel alive for 5 hours run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)