diff --git a/.github/workflows/personal-staging-hourly.yml b/.github/workflows/personal-staging-hourly.yml index be9da235a2..d1955cfb9d 100644 --- a/.github/workflows/personal-staging-hourly.yml +++ b/.github/workflows/personal-staging-hourly.yml @@ -42,6 +42,7 @@ jobs: # from pushing fork main with this job's write token. if: github.repository == 'btli/omnigent' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest + environment: staging-push timeout-minutes: 10 permissions: contents: write @@ -49,10 +50,16 @@ jobs: - name: Checkout fork main with full history uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + # App tokens cannot push workflow-file changes; prefer the + # workflows-scoped PAT, with GitHub token as fallback. + # PAT pushes trigger workflows, so PAT merges force a [skip ci] commit. + token: ${{ secrets.STAGING_PUSH_TOKEN || github.token }} ref: main fetch-depth: 0 - name: Merge upstream main and push (soft-fail on conflict) + env: + HAS_PAT: ${{ secrets.STAGING_PUSH_TOKEN != '' }} run: | set -euo pipefail git config user.name "github-actions[bot]" @@ -70,8 +77,10 @@ jobs: # (unmerged index entries) are skippable. A bad object or a transport # error must stay loud instead of hiding behind an hourly warning. merge_or_soft_fail() { - if git merge --no-edit "$1"; then - return 0 + if [ "$HAS_PAT" = "true" ]; then + git merge --no-ff -m "Merge upstream main [skip ci]" "$1" && return 0 + else + git merge --no-edit "$1" && return 0 fi if [ -z "$(git ls-files -u)" ]; then echo "::error::sync-main: git merge failed with no conflicting paths — not a content conflict" @@ -153,6 +162,7 @@ jobs: # security boundary — see the nightly workflow's integrate job. if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + environment: staging-push timeout-minutes: 20 permissions: contents: write @@ -160,6 +170,9 @@ jobs: - name: Checkout fork with full history uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + # App tokens cannot push workflow-file changes; prefer the + # workflows-scoped PAT, with GitHub token as fallback. + token: ${{ secrets.STAGING_PUSH_TOKEN || github.token }} fetch-depth: 0 - name: Compose staging from upstream main + open btli PRs + extras diff --git a/.github/workflows/personal-staging.yml b/.github/workflows/personal-staging.yml index 93f98cb661..ea55cbfd6c 100644 --- a/.github/workflows/personal-staging.yml +++ b/.github/workflows/personal-staging.yml @@ -11,7 +11,9 @@ # credentials unpersisted and may not write shared caches. The privileged # integrate job runs only the stdlib composer; the keystore only ever appears # in android-sign, which re-signs the APK unconditionally so the untrusted -# build's own signature never ships. +# build's own signature never ships. PAT-bearing jobs use the main-only +# staging-push environment so staging-tree workflows cannot receive the PAT; +# dev-tag pushes may enqueue release/image workflows, whose repo guards skip. name: Personal Staging Nightly on: @@ -35,8 +37,9 @@ jobs: # HEAD directly and never reads fork main. sync-main: # Fork-only, mirroring upstream's `if: github.repository == 'omnigent-ai/omnigent'` guards. - if: github.repository == 'btli/omnigent' + if: github.repository == 'btli/omnigent' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest + environment: staging-push timeout-minutes: 10 permissions: contents: write @@ -44,17 +47,27 @@ jobs: - name: Checkout fork main with full history uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + # App tokens cannot push workflow-file changes; prefer the + # workflows-scoped PAT, with GitHub token as fallback. + token: ${{ secrets.STAGING_PUSH_TOKEN || github.token }} ref: main fetch-depth: 0 - name: Merge upstream main and push + env: + HAS_PAT: ${{ secrets.STAGING_PUSH_TOKEN != '' }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git remote add upstream https://github.com/omnigent-ai/omnigent.git git fetch upstream main - if ! git merge --no-edit FETCH_HEAD; then + if [ "$HAS_PAT" = "true" ]; then + merge_command=(git merge --no-ff -m "Merge upstream main [skip ci]" FETCH_HEAD) + else + merge_command=(git merge --no-edit FETCH_HEAD) + fi + if ! "${merge_command[@]}"; then { echo "## sync-main: merge conflict with upstream main" echo "" @@ -113,6 +126,7 @@ jobs: # server-enforced staging-signing Environment on android-sign. if: always() && needs.test-composer.result == 'success' && needs.sync-main.result != 'cancelled' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest + environment: staging-push timeout-minutes: 20 permissions: contents: write @@ -124,6 +138,9 @@ jobs: - name: Checkout fork with full history uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + # App tokens cannot push workflow-file changes; prefer the + # workflows-scoped PAT, with GitHub token as fallback. + token: ${{ secrets.STAGING_PUSH_TOKEN || github.token }} fetch-depth: 0 - name: Compose staging from upstream main + open btli PRs @@ -350,6 +367,7 @@ jobs: needs: [integrate, android-sign] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + environment: staging-push timeout-minutes: 15 permissions: contents: write @@ -415,6 +433,10 @@ jobs: # commit; the release is then edited in place (no delete/recreate, so # the stable releases/download/nightly-latest/ URL never 404s). - name: Update nightly-latest + env: + # App tokens cannot update refs containing workflow-file changes; + # prefer the workflows-scoped PAT, with GitHub token as fallback. + GH_TOKEN: ${{ secrets.STAGING_PUSH_TOKEN || github.token }} run: | set -euo pipefail gh api -X PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/nightly-latest" \