Merge pull request #193 from claude/security-audit-workflows
Workflows: deploy secrets in the production environment, Deploy only from main's tip, step-scoped Cloudflare secrets, pinned pnpm action
cnbrkdmrci authored 2 parents eb6a15bb268bcecommit 9702f3ce113c04f1d16eb39e7ed1d9ce92d09f73Browse files
- CI / Typecheck and bundlesuccess
- CI / Unit tests (1/2)success
- CI / Unit tests (2/2)Failed
- End-to-end / End-to-end (agent reviews, tasks, Today)Failed
- End-to-end / End-to-end (git, PRs, Actions)Failed
- End-to-end / End-to-end (large pushes)Failed
- End-to-end / End-to-end (passkeys, registry, email)Failed
- End-to-end / End-to-end (real histories)Failed
- End-to-end / End-to-end (security)Failed
8 files changed+113−47
Files (8)
modified.github/workflows/ci.yml+2−2View file
| @@ -24,7 +24,7 @@ | ||
| 24 | 24 | timeout-minutes: 20 |
| 25 | 25 | steps: |
| 26 | 26 | - uses: actions/checkout@v4 |
| 27 | - - uses: pnpm/action-setup@v4 | |
| 27 | + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | |
| 28 | 28 | - uses: actions/setup-node@v4 |
| 29 | 29 | with: |
| 30 | 30 | node-version: 22 |
| @@ -49,7 +49,7 @@ | ||
| 49 | 49 | shard: [1, 2] |
| 50 | 50 | steps: |
| 51 | 51 | - uses: actions/checkout@v4 |
| 52 | - - uses: pnpm/action-setup@v4 | |
| 52 | + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | |
| 53 | 53 | - uses: actions/setup-node@v4 |
| 54 | 54 | with: |
| 55 | 55 | node-version: 22 |
modified.github/workflows/d1-replication.yml+3−1View file
| @@ -19,6 +19,8 @@ | ||
| 19 | 19 | check: |
| 20 | 20 | name: Check sessions on a test database |
| 21 | 21 | runs-on: ubuntu-latest |
| 22 | + # The Cloudflare secrets are the production environment's. | |
| 23 | + environment: production | |
| 22 | 24 | timeout-minutes: 20 |
| 23 | 25 | env: |
| 24 | 26 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} |
| @@ -26,7 +28,7 @@ | ||
| 26 | 28 | NAME: diffinery-d1-probe-${{ github.run_id }} |
| 27 | 29 | steps: |
| 28 | 30 | - uses: actions/checkout@v4 |
| 29 | - - uses: pnpm/action-setup@v4 | |
| 31 | + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | |
| 30 | 32 | - uses: actions/setup-node@v4 |
| 31 | 33 | with: |
| 32 | 34 | node-version: 22 |
modified.github/workflows/deploy.yml+56−5View file
| @@ -10,6 +10,11 @@ | ||
| 10 | 10 | # |
| 11 | 11 | # It deploys a main commit only once CI (typecheck, unit tests and the Worker bundle) has |
| 12 | 12 | # passed on it, so it doesn't run those itself. End-to-end tests run alongside the deploy. |
| 13 | +# Only main's current tip deploys: re-running an old CI run (which anyone with write access | |
| 14 | +# can do) doesn't bring back a version from before a fix. | |
| 15 | +# | |
| 16 | +# The Cloudflare secrets are the production environment's, and only the steps that talk to | |
| 17 | +# Cloudflare get them: installs, builds and third-party actions run without them. | |
| 13 | 18 | on: |
| 14 | 19 | workflow_run: |
| 15 | 20 | workflows: [CI] |
| @@ -25,13 +30,43 @@ | ||
| 25 | 30 | cancel-in-progress: false |
| 26 | 31 | |
| 27 | 32 | jobs: |
| 28 | - deploy: | |
| 29 | - name: Deploy to Cloudflare | |
| 33 | + gate: | |
| 34 | + name: Main's current tip | |
| 30 | 35 | # Production deploys from Diffinery itself, on its own hosted runners (any |
| 31 | 36 | # Diffinery, so self-hosted copies deploy themselves too). Running this |
| 32 | 37 | # workflow by hand on GitHub still deploys, as a fallback. |
| 33 | 38 | if: ${{ github.event_name == 'workflow_dispatch' || (github.server_url != 'https://github.com' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push') }} |
| 34 | 39 | runs-on: ubuntu-latest |
| 40 | + timeout-minutes: 5 | |
| 41 | + outputs: | |
| 42 | + deploy: ${{ steps.tip.outputs.deploy }} | |
| 43 | + steps: | |
| 44 | + - name: Deploy only main's current tip | |
| 45 | + id: tip | |
| 46 | + env: | |
| 47 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 48 | + WANT: ${{ github.event.workflow_run.head_sha || github.sha }} | |
| 49 | + REF: ${{ github.ref }} | |
| 50 | + run: | | |
| 51 | + set -euo pipefail | |
| 52 | + if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ] && [ "$REF" != refs/heads/main ]; then | |
| 53 | + echo "::error::Deploy runs from main only (this run is on $REF)"; exit 1 | |
| 54 | + fi | |
| 55 | + tip=$(curl -fsS "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/branches/main" -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" | jq -r .commit.sha) | |
| 56 | + if [ "$tip" = "$WANT" ]; then | |
| 57 | + echo "deploy=true" >> "$GITHUB_OUTPUT" | |
| 58 | + echo "deploying $WANT, main's tip" | |
| 59 | + else | |
| 60 | + # A later commit's CI run deploys it: a re-run of an old CI run deploys nothing. | |
| 61 | + echo "deploy=false" >> "$GITHUB_OUTPUT" | |
| 62 | + echo "::notice::not deploying $WANT: main is at $tip now, and that commit's own run deploys it" | |
| 63 | + fi | |
| 64 | + | |
| 65 | + deploy: | |
| 66 | + name: Deploy to Cloudflare | |
| 67 | + needs: gate | |
| 68 | + if: ${{ needs.gate.outputs.deploy == 'true' }} | |
| 69 | + runs-on: ubuntu-latest | |
| 35 | 70 | timeout-minutes: 45 |
| 36 | 71 | environment: |
| 37 | 72 | name: production |
| @@ -40,15 +75,13 @@ | ||
| 40 | 75 | DIFFINERY_DOMAIN: ${{ vars.DIFFINERY_DOMAIN || 'diffinery.com' }} |
| 41 | 76 | DIFFINERY_ZONE: ${{ vars.DIFFINERY_ZONE }} |
| 42 | 77 | MAIL_DOMAIN: ${{ vars.MAIL_DOMAIN }} |
| 43 | - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 44 | - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| 45 | 78 | ACCESS_TEAM_DOMAIN: ${{ vars.ACCESS_TEAM_DOMAIN }} |
| 46 | 79 | steps: |
| 47 | 80 | - uses: actions/checkout@v4 |
| 48 | 81 | with: |
| 49 | 82 | # The commit CI passed on, not whatever main is by now. |
| 50 | 83 | ref: ${{ github.event.workflow_run.head_sha || github.sha }} |
| 51 | - - uses: pnpm/action-setup@v4 | |
| 84 | + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | |
| 52 | 85 | - uses: actions/setup-node@v4 |
| 53 | 86 | with: |
| 54 | 87 | node-version: 22 |
| @@ -57,27 +90,45 @@ | ||
| 57 | 90 | - name: Build runner agent and web islands |
| 58 | 91 | run: pnpm -r --if-present build |
| 59 | 92 | - name: Runner image (built only when the runner changed) |
| 93 | + env: | |
| 94 | + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 95 | + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| 60 | 96 | run: scripts/runner-image.sh |
| 61 | 97 | |
| 62 | 98 | - name: Create or update Cloudflare resources |
| 63 | 99 | id: bootstrap |
| 100 | + env: | |
| 101 | + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 102 | + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| 64 | 103 | run: scripts/bootstrap.sh |
| 65 | 104 | |
| 66 | 105 | - name: Deploy Worker and runner image |
| 67 | 106 | working-directory: apps/diffinery |
| 107 | + env: | |
| 108 | + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 109 | + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| 68 | 110 | run: pnpm exec wrangler deploy -c wrangler.deploy.jsonc |
| 69 | 111 | |
| 70 | 112 | - name: Wait for new containers to get the new image |
| 113 | + env: | |
| 114 | + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 115 | + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| 71 | 116 | # The Live test follows this workflow, and its checks must run on the new image. |
| 72 | 117 | # This job runs on an old-image container, so finishing after the rollout's last step |
| 73 | 118 | # also shows that a deploy doesn't stop a running job. |
| 74 | 119 | run: scripts/wait-container-rollout.sh |
| 75 | 120 | |
| 76 | 121 | - name: Worker secrets (generated on the first deploy; TURNSTILE_SECRET when missing) |
| 122 | + env: | |
| 123 | + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 124 | + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| 77 | 125 | run: scripts/ensure-secrets.sh |
| 78 | 126 | |
| 79 | 127 | - name: Finish Access setup on first deploy |
| 80 | 128 | if: steps.bootstrap.outputs.access_team == '' |
| 129 | + env: | |
| 130 | + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 131 | + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| 81 | 132 | run: | |
| 82 | 133 | scripts/bootstrap.sh |
| 83 | 134 | cd apps/diffinery && pnpm exec wrangler deploy -c wrangler.deploy.jsonc --containers-rollout none |
modified.github/workflows/dind-probe.yml+3−1View file
| @@ -14,6 +14,8 @@ | ||
| 14 | 14 | probe: |
| 15 | 15 | name: Probe Docker inside a container |
| 16 | 16 | runs-on: ubuntu-latest |
| 17 | + # The Cloudflare secrets are the production environment's. | |
| 18 | + environment: production | |
| 17 | 19 | timeout-minutes: 40 |
| 18 | 20 | env: |
| 19 | 21 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} |
| @@ -21,7 +23,7 @@ | ||
| 21 | 23 | NAME: diffinery-dind-probe-${{ github.run_id }} |
| 22 | 24 | steps: |
| 23 | 25 | - uses: actions/checkout@v4 |
| 24 | - - uses: pnpm/action-setup@v4 | |
| 26 | + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | |
| 25 | 27 | - uses: actions/setup-node@v4 |
| 26 | 28 | with: |
| 27 | 29 | node-version: 22 |
modified.github/workflows/e2e.yml+1−1View file
| @@ -39,7 +39,7 @@ | ||
| 39 | 39 | shard: rest |
| 40 | 40 | steps: |
| 41 | 41 | - uses: actions/checkout@v4 |
| 42 | - - uses: pnpm/action-setup@v4 | |
| 42 | + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | |
| 43 | 43 | - uses: actions/setup-node@v4 |
| 44 | 44 | with: |
| 45 | 45 | node-version: 22 |
modified.github/workflows/import-campaign.yml+6−23View file
| @@ -5,10 +5,8 @@ | ||
| 5 | 5 | # came across and what ran (scripts/import-campaign.sh). Started by hand. |
| 6 | 6 | # |
| 7 | 7 | # Dispatched on GitHub, it starts the same workflow on Diffinery and prints its report; |
| 8 | -# on Diffinery it does the work. Imports use the IMPORT_GITHUB_TOKEN secret when it is | |
| 9 | -# set (a read-only token for public repositories), otherwise GitHub's anonymous rate | |
| 10 | -# limit applies and large repositories take hours. Set on GitHub, it is copied to | |
| 11 | -# Diffinery's repository secrets before the run starts (the main sync doesn't copy it). | |
| 8 | +# on Diffinery it does the work. Imports run token-free (Diffinery keeps no GitHub | |
| 9 | +# credentials), so GitHub's anonymous rate limit applies and large repositories take hours. | |
| 12 | 10 | on: |
| 13 | 11 | workflow_dispatch: |
| 14 | 12 | inputs: |
| @@ -66,20 +64,8 @@ | ||
| 66 | 64 | env: |
| 67 | 65 | DIFFINERY: https://diffinery.com |
| 68 | 66 | TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }} |
| 69 | - R: codician-team/diffinery | |
| 67 | + R: codician/diffinery | |
| 70 | 68 | steps: |
| 71 | - - name: Copy IMPORT_GITHUB_TOKEN to Diffinery | |
| 72 | - if: ${{ inputs.follow_run == '' && !inputs.peek && inputs.rerun_failed == '' && inputs.inspect_run == '' && inputs.push_test == '' }} | |
| 73 | - env: | |
| 74 | - IMPORT_GITHUB_TOKEN: ${{ secrets.IMPORT_GITHUB_TOKEN }} | |
| 75 | - run: | | |
| 76 | - set -euo pipefail | |
| 77 | - if [ -n "$IMPORT_GITHUB_TOKEN" ]; then | |
| 78 | - jq -n --arg v "$IMPORT_GITHUB_TOKEN" '{value: $v}' | curl -fsS --retry 4 --retry-delay 30 -o /dev/null -X PUT "$DIFFINERY/api/v3/repos/$R/actions/secrets/IMPORT_GITHUB_TOKEN" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" --data-binary @- | |
| 79 | - echo "IMPORT_GITHUB_TOKEN is set on GitHub and was copied to Diffinery" | |
| 80 | - else | |
| 81 | - echo "IMPORT_GITHUB_TOKEN is not set on GitHub; Diffinery's own copy is used if it has one" | |
| 82 | - fi | |
| 83 | 69 | - name: Dispatch and follow |
| 84 | 70 | env: |
| 85 | 71 | REPOS: ${{ inputs.repos }} |
| @@ -229,10 +215,11 @@ | ||
| 229 | 215 | if: ${{ github.server_url != 'https://github.com' }} |
| 230 | 216 | runs-on: ubuntu-latest |
| 231 | 217 | timeout-minutes: 360 |
| 218 | + # DIFFINERY_LIVE_TOKEN is the production environment's. | |
| 219 | + environment: production | |
| 232 | 220 | env: |
| 233 | 221 | DIFFINERY: ${{ github.server_url }} |
| 234 | 222 | TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }} |
| 235 | - GITHUB_TOKEN_FOR_IMPORT: ${{ secrets.IMPORT_GITHUB_TOKEN }} | |
| 236 | 223 | steps: |
| 237 | 224 | - uses: actions/checkout@v4 |
| 238 | 225 | - name: Import and run |
| @@ -241,11 +228,7 @@ | ||
| 241 | 228 | MINUTES: ${{ inputs.minutes }} |
| 242 | 229 | RUN_WORKFLOWS: ${{ inputs.run_workflows }} |
| 243 | 230 | run: | |
| 244 | - if [ -n "$GITHUB_TOKEN_FOR_IMPORT" ]; then | |
| 245 | - echo "::notice::Mode: imports use the IMPORT_GITHUB_TOKEN secret" | |
| 246 | - else | |
| 247 | - echo "::warning::Mode: token-free. IMPORT_GITHUB_TOKEN is not set, so imports use GitHub's anonymous rate limit" | |
| 248 | - fi | |
| 231 | + echo "::notice::Mode: token-free, so imports use GitHub's anonymous rate limit" | |
| 249 | 232 | # shellcheck disable=SC2086 |
| 250 | 233 | REPORT=report.md scripts/import-campaign.sh $REPOS |
| 251 | 234 | cat report.md >> "$GITHUB_STEP_SUMMARY" |
modified.github/workflows/live-test.yml+35−8View file
| @@ -6,8 +6,10 @@ | ||
| 6 | 6 | # queue). Each run works in a throwaway private repository that it |
| 7 | 7 | # deletes at the end. |
| 8 | 8 | # |
| 9 | -# Needs the repository secret DIFFINERY_LIVE_TOKEN: a Diffinery token (Settings -> | |
| 10 | -# Tokens) with the admin scope and access to all repositories. | |
| 9 | +# Needs DIFFINERY_LIVE_TOKEN in the production environment: a write-scope token | |
| 10 | +# (Settings -> Tokens) with access to all repositories, for a dedicated account that isn't | |
| 11 | +# a site administrator (give it a quota override). The checks that need a site | |
| 12 | +# administrator are skipped with a notice when the token isn't one's. | |
| 11 | 13 | on: |
| 12 | 14 | workflow_dispatch: |
| 13 | 15 | workflow_run: |
| @@ -17,6 +19,8 @@ | ||
| 17 | 19 | |
| 18 | 20 | permissions: |
| 19 | 21 | contents: read |
| 22 | + # To see whether the Deploy run it follows deployed anything. | |
| 23 | + actions: read | |
| 20 | 24 | |
| 21 | 25 | concurrency: |
| 22 | 26 | group: live-test |
| @@ -29,6 +33,7 @@ | ||
| 29 | 33 | if: ${{ github.event_name == 'workflow_dispatch' || (github.server_url == 'https://diffinery.com' && github.event.workflow_run.conclusion == 'success') }} |
| 30 | 34 | runs-on: ubuntu-latest |
| 31 | 35 | timeout-minutes: 45 |
| 36 | + environment: production | |
| 32 | 37 | env: |
| 33 | 38 | DIFFINERY: https://diffinery.com |
| 34 | 39 | TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }} |
| @@ -37,9 +42,27 @@ | ||
| 37 | 42 | steps: |
| 38 | 43 | - name: Check the token |
| 39 | 44 | id: token |
| 45 | + env: | |
| 46 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 47 | + DEPLOY_RUN: ${{ github.event.workflow_run.id }} | |
| 40 | 48 | run: | |
| 49 | + if [ "$GITHUB_EVENT_NAME" = workflow_run ]; then | |
| 50 | + # Deploy's gate skips a commit that is no longer main's tip (the newer commit's run deploys it): | |
| 51 | + # then nothing new is live, and this run would only test the site as it was. | |
| 52 | + deployed=$(curl -fsS "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/runs/$DEPLOY_RUN/jobs" -H "Authorization: Bearer $GH_TOKEN" | jq -r '[.jobs[] | select(.name == "Deploy to Cloudflare") | .conclusion][0] // "unknown"' || echo unknown) | |
| 53 | + if [ "$deployed" = skipped ]; then | |
| 54 | + echo "::notice::Deploy deployed nothing for $HEAD_SHA (main has moved on), so there is nothing new to test" | |
| 55 | + echo "skip=true" >> "$GITHUB_OUTPUT" | |
| 56 | + exit 0 | |
| 57 | + fi | |
| 58 | + fi | |
| 41 | 59 | if [ -z "$TOKEN" ]; then |
| 42 | - echo "::warning::DIFFINERY_LIVE_TOKEN is not set, so the live test is skipped. Create an admin token at $DIFFINERY/settings/tokens and save it as a repository secret." | |
| 60 | + # After a deploy of diffinery.com a missing token is an error: a green run would mean nothing was tested. | |
| 61 | + if [ "$GITHUB_EVENT_NAME" = workflow_run ]; then | |
| 62 | + echo "::error::DIFFINERY_LIVE_TOKEN is not set in the production environment, so nothing was tested. Create a write token at $DIFFINERY/settings/tokens and save it as a production environment secret." | |
| 63 | + exit 1 | |
| 64 | + fi | |
| 65 | + echo "::warning::DIFFINERY_LIVE_TOKEN is not set, so the live test is skipped. Create a write token at $DIFFINERY/settings/tokens and save it as a production environment secret." | |
| 43 | 66 | echo "skip=true" >> "$GITHUB_OUTPUT" |
| 44 | 67 | exit 0 |
| 45 | 68 | fi |
| @@ -145,7 +168,11 @@ | ||
| 145 | 168 | if: steps.token.outputs.skip != 'true' |
| 146 | 169 | run: | |
| 147 | 170 | set -euo pipefail |
| 148 | - s=$(curl -fsS "$DIFFINERY/api/v3/diffinery/admin/email" -H "Authorization: Bearer $TOKEN") | |
| 171 | + s=$(curl -sS -w '\n%{http_code}' "$DIFFINERY/api/v3/diffinery/admin/email" -H "Authorization: Bearer $TOKEN") | |
| 172 | + code=${s##*$'\n'}; s=${s%$'\n'*} | |
| 173 | + # Only a site administrator's token reads the email settings, and the live-test account isn't one. | |
| 174 | + if [ "$code" = 403 ]; then echo "::notice::skipped: the live-test token isn't a site administrator's"; exit 0; fi | |
| 175 | + [ "$code" = 200 ] || { echo "::error::/diffinery/admin/email answered $code"; exit 1; } | |
| 149 | 176 | echo "$s" | jq '{sending, queue, from, last_24h}' |
| 150 | 177 | [ "$(jq -r .sending <<<"$s")" = true ] || { echo "::error::the Worker has no Email Sending binding"; exit 1; } |
| 151 | 178 | [ "$(jq -r .queue <<<"$s")" = true ] || { echo "::error::the Worker has no mail queue binding"; exit 1; } |
| @@ -678,15 +705,14 @@ | ||
| 678 | 705 | env: |
| 679 | 706 | # A small public repository with issues and pull requests; a repository variable can change it. |
| 680 | 707 | SOURCE: ${{ vars.LIVE_IMPORT_SOURCE || 'octocat/git-consortium' }} |
| 681 | - # A read-only GitHub token (copied here by the sync), so GitHub's rate limit doesn't skip the check. Never printed. | |
| 682 | - GITHUB_TOKEN_FOR_IMPORT: ${{ secrets.IMPORT_GITHUB_TOKEN }} | |
| 708 | + # Token-free: Diffinery keeps no GitHub credentials of its own (the importer takes a person's token when they import). | |
| 709 | + GITHUB_TOKEN_FOR_IMPORT: "" | |
| 683 | 710 | run: | |
| 684 | 711 | set -euo pipefail |
| 685 | 712 | api() { curl -fsS -X "$1" "$DIFFINERY/api/v3$2" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" ${3:+--data "$3"}; } |
| 686 | 713 | name="$REPO-import" |
| 687 | 714 | body=$(jq -nc --arg s "$SOURCE" --arg o "$OWNER" --arg n "$name" --arg t "${GITHUB_TOKEN_FOR_IMPORT:-}" \ |
| 688 | 715 | '{source: $s, owner: $o, name: $n, visibility: "private"} + (if $t == "" then {} else {github_token: $t} end)') |
| 689 | - if [ -n "${GITHUB_TOKEN_FOR_IMPORT:-}" ]; then echo "importing with IMPORT_GITHUB_TOKEN"; else echo "::warning::IMPORT_GITHUB_TOKEN is not set, so the import runs token-free"; fi | |
| 690 | 716 | # Token-free, GitHub allows 60 requests an hour per address: a rate limit skips this check with a warning. |
| 691 | 717 | # Starting the import reads the repository from GitHub; a GitHub outage (502 "could not reach GitHub") |
| 692 | 718 | # is retried, and skips this check with a warning if it outlasts the retries. While other imports hold |
| @@ -770,7 +796,8 @@ | ||
| 770 | 796 | sleep 10 |
| 771 | 797 | done |
| 772 | 798 | if [ -z "$run" ]; then |
| 773 | - # Whether the scheduler stopped ticking (last_tick_at is old) or ticked without starting the run. | |
| 799 | + # Whether the scheduler stopped ticking (last_tick_at is old) or ticked without starting the run | |
| 800 | + # (site administrators only: another token prints the 403). | |
| 774 | 801 | echo "scheduler: $(curl -sS "$DIFFINERY/api/v3/diffinery/admin/scheduler" -H "Authorization: Bearer $TOKEN") (now $(date -u +%FT%TZ))" |
| 775 | 802 | echo "::error::no scheduled run started" |
| 776 | 803 | exit 1 |
modified.github/workflows/sync-to-diffinery.yml+7−6View file
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | name: Sync to Diffinery |
| 2 | 2 | |
| 3 | -# Keeps diffinery.com/codician-team/diffinery in step with this repository: every | |
| 3 | +# Kept diffinery.com/codician-team/diffinery (now codician/diffinery) in step with this repository: every | |
| 4 | 4 | # branch and tag, plus the Actions secrets and variables our workflows use. On the |
| 5 | 5 | # first run it creates the organization and repository. For main it then waits for |
| 6 | 6 | # the Actions runs on Diffinery and fails if any of them fail. |
| @@ -8,9 +8,10 @@ | ||
| 8 | 8 | # Secrets go straight from GitHub's secret store to Diffinery's over HTTPS and are |
| 9 | 9 | # never printed. Needs DIFFINERY_LIVE_TOKEN (an admin token on diffinery.com). |
| 10 | 10 | # |
| 11 | -# Manual only since the cut-off: Diffinery (codician/diffinery) is this repository's home, | |
| 12 | -# GitHub's copy is frozen, and pushes here are no longer copied over. The file stays as a | |
| 13 | -# record and for one last manual run if ever needed. | |
| 11 | +# Retired since the cut-off: Diffinery (codician/diffinery) is this repository's home, | |
| 12 | +# GitHub's copy is frozen, and nothing is copied over any more. The file stays as a record | |
| 13 | +# only; its job never runs (it would copy GitHub's frozen branches over Diffinery's and write | |
| 14 | +# repository-level secrets, which now live in the production environment). | |
| 14 | 15 | on: |
| 15 | 16 | workflow_dispatch: |
| 16 | 17 | inputs: |
| @@ -40,14 +41,14 @@ | ||
| 40 | 41 | name: Copy code, secrets and variables, then check Actions on Diffinery |
| 41 | 42 | # Runs on GitHub only; the copy on Diffinery has nothing to sync to. |
| 42 | 43 | # Branch deletions aren't copied (Diffinery keeps the branch until it's deleted there). |
| 43 | - if: ${{ github.server_url == 'https://github.com' && !github.event.deleted }} | |
| 44 | + if: false | |
| 44 | 45 | runs-on: ubuntu-latest |
| 45 | 46 | timeout-minutes: 90 |
| 46 | 47 | environment: production |
| 47 | 48 | env: |
| 48 | 49 | DIFFINERY: https://diffinery.com |
| 49 | 50 | TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }} |
| 50 | - ORG: codician-team | |
| 51 | + ORG: codician | |
| 51 | 52 | NAME: diffinery |
| 52 | 53 | steps: |
| 53 | 54 | - name: Check the token |