ci: use the marketplace actions with plain @vN tags, as a normal job wouldUnverified
Back to actions/checkout@v4, actions/setup-go@v5, actions/upload-artifact@v4 and softprops/action-gh-release@v2, and drop scripts/publish-release.sh. These workflows are meant to read as an ordinary GitHub Actions job; anonymous action fetches being rate-limited on the fleet is a runner configuration matter (RICKUB_ACTIONS_GITHUB_TOKEN), documented in RELEASING.md rather than worked around here. Keeps the GITHUB_WORKFLOW fix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgkcT3UPxwxukBKojB5eg4
900561e parent: 5bbf166 modified
.github/workflows/ci.yml +2 -2 | @@ -23,10 +23,10 @@ jobs: | ||
| 23 | 23 | runs-on: ubuntu-latest |
| 24 | 24 | steps: |
| 25 | 25 | - name: Checkout |
| 26 | - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| 26 | + uses: actions/checkout@v4 | |
| 27 | 27 | |
| 28 | 28 | - name: Set up Go |
| 29 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| 29 | + uses: actions/setup-go@v5 | |
| 30 | 30 | with: |
| 31 | 31 | go-version-file: go.mod |
| 32 | 32 | cache: true |
| @@ -23,10 +23,10 @@ jobs: | |||
| 23 | runs-on: ubuntu-latest | 23 | runs-on: ubuntu-latest |
| 24 | steps: | 24 | steps: |
| 25 | - name: Checkout | 25 | - name: Checkout |
| 26 | - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | 26 | + uses: actions/checkout@v4 |
| 27 | 27 | ||
| 28 | - name: Set up Go | 28 | - name: Set up Go |
| 29 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | 29 | + uses: actions/setup-go@v5 |
| 30 | with: | 30 | with: |
| 31 | go-version-file: go.mod | 31 | go-version-file: go.mod |
| 32 | cache: true | 32 | cache: true |
modified
.github/workflows/rc.yml +17 -12 | @@ -26,10 +26,10 @@ jobs: | ||
| 26 | 26 | runs-on: ubuntu-latest |
| 27 | 27 | steps: |
| 28 | 28 | - name: Checkout |
| 29 | - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| 29 | + uses: actions/checkout@v4 | |
| 30 | 30 | |
| 31 | 31 | - name: Set up Go |
| 32 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| 32 | + uses: actions/setup-go@v5 | |
| 33 | 33 | with: |
| 34 | 34 | go-version-file: go.mod |
| 35 | 35 | cache: true |
| @@ -89,7 +89,7 @@ jobs: | ||
| 89 | 89 | echo "path=${RUNNER_TEMP}/rc-notes.md" >> "$GITHUB_OUTPUT" |
| 90 | 90 | |
| 91 | 91 | - name: Upload build artifacts |
| 92 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| 92 | + uses: actions/upload-artifact@v4 | |
| 93 | 93 | with: |
| 94 | 94 | name: rickub-${{ steps.version.outputs.version }} |
| 95 | 95 | path: dist/ |
| @@ -97,12 +97,17 @@ jobs: | ||
| 97 | 97 | retention-days: 14 |
| 98 | 98 | |
| 99 | 99 | - name: Publish prerelease |
| 100 | - env: | |
| 101 | - TAG: ${{ steps.version.outputs.tag }} | |
| 102 | - NOTES: ${{ steps.notes.outputs.path }} | |
| 103 | - # The tag does not exist yet: the release shim creates it at --target, | |
| 104 | - # so CI cuts its own RC tags. | |
| 105 | - run: | | |
| 106 | - bash scripts/publish-release.sh --prerelease \ | |
| 107 | - --tag "${TAG}" --target "${GITHUB_SHA}" --notes "${NOTES}" \ | |
| 108 | - dist/*.tar.gz dist/SHA256SUMS | |
| 100 | + uses: softprops/action-gh-release@v2 | |
| 101 | + with: | |
| 102 | + tag_name: ${{ steps.version.outputs.tag }} | |
| 103 | + name: ${{ steps.version.outputs.tag }} | |
| 104 | + body_path: ${{ steps.notes.outputs.path }} | |
| 105 | + # The tag does not exist yet: the rickub release shim creates it at | |
| 106 | + # target_commitish, so CI cuts its own RC tags. | |
| 107 | + target_commitish: ${{ github.sha }} | |
| 108 | + draft: false | |
| 109 | + prerelease: true | |
| 110 | + files: | | |
| 111 | + dist/*.tar.gz | |
| 112 | + dist/SHA256SUMS | |
| 113 | + fail_on_unmatched_files: true | |
| @@ -26,10 +26,10 @@ jobs: | |||
| 26 | runs-on: ubuntu-latest | 26 | runs-on: ubuntu-latest |
| 27 | steps: | 27 | steps: |
| 28 | - name: Checkout | 28 | - name: Checkout |
| 29 | - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | 29 | + uses: actions/checkout@v4 |
| 30 | 30 | ||
| 31 | - name: Set up Go | 31 | - name: Set up Go |
| 32 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | 32 | + uses: actions/setup-go@v5 |
| 33 | with: | 33 | with: |
| 34 | go-version-file: go.mod | 34 | go-version-file: go.mod |
| 35 | cache: true | 35 | cache: true |
| @@ -89,7 +89,7 @@ jobs: | |||
| 89 | echo "path=${RUNNER_TEMP}/rc-notes.md" >> "$GITHUB_OUTPUT" | 89 | echo "path=${RUNNER_TEMP}/rc-notes.md" >> "$GITHUB_OUTPUT" |
| 90 | 90 | ||
| 91 | - name: Upload build artifacts | 91 | - name: Upload build artifacts |
| 92 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | 92 | + uses: actions/upload-artifact@v4 |
| 93 | with: | 93 | with: |
| 94 | name: rickub-${{ steps.version.outputs.version }} | 94 | name: rickub-${{ steps.version.outputs.version }} |
| 95 | path: dist/ | 95 | path: dist/ |
| @@ -97,12 +97,17 @@ jobs: | |||
| 97 | retention-days: 14 | 97 | retention-days: 14 |
| 98 | 98 | ||
| 99 | - name: Publish prerelease | 99 | - name: Publish prerelease |
| 100 | - env: | 100 | + uses: softprops/action-gh-release@v2 |
| 101 | - TAG: ${{ steps.version.outputs.tag }} | 101 | + with: |
| 102 | - NOTES: ${{ steps.notes.outputs.path }} | 102 | + tag_name: ${{ steps.version.outputs.tag }} |
| 103 | - # The tag does not exist yet: the release shim creates it at --target, | 103 | + name: ${{ steps.version.outputs.tag }} |
| 104 | - # so CI cuts its own RC tags. | 104 | + body_path: ${{ steps.notes.outputs.path }} |
| 105 | - run: | | 105 | + # The tag does not exist yet: the rickub release shim creates it at |
| 106 | - bash scripts/publish-release.sh --prerelease \ | 106 | + # target_commitish, so CI cuts its own RC tags. |
| 107 | - --tag "${TAG}" --target "${GITHUB_SHA}" --notes "${NOTES}" \ | 107 | + target_commitish: ${{ github.sha }} |
| 108 | - dist/*.tar.gz dist/SHA256SUMS | 108 | + draft: false |
| 109 | + prerelease: true | ||
| 110 | + files: | | ||
| 111 | + dist/*.tar.gz | ||
| 112 | + dist/SHA256SUMS | ||
| 113 | + fail_on_unmatched_files: true | ||
modified
.github/workflows/release.yml +17 -12 | @@ -36,7 +36,7 @@ jobs: | ||
| 36 | 36 | runs-on: ubuntu-latest |
| 37 | 37 | steps: |
| 38 | 38 | - name: Checkout |
| 39 | - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| 39 | + uses: actions/checkout@v4 | |
| 40 | 40 | with: |
| 41 | 41 | # Full history + tags so the "tag already exists" check is meaningful. |
| 42 | 42 | fetch-depth: 0 |
| @@ -133,7 +133,7 @@ jobs: | ||
| 133 | 133 | esac |
| 134 | 134 | |
| 135 | 135 | - name: Set up Go |
| 136 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| 136 | + uses: actions/setup-go@v5 | |
| 137 | 137 | with: |
| 138 | 138 | go-version-file: go.mod |
| 139 | 139 | cache: true |
| @@ -180,7 +180,7 @@ jobs: | ||
| 180 | 180 | echo "path=${RUNNER_TEMP}/release-notes.md" >> "$GITHUB_OUTPUT" |
| 181 | 181 | |
| 182 | 182 | - name: Upload build artifacts |
| 183 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| 183 | + uses: actions/upload-artifact@v4 | |
| 184 | 184 | with: |
| 185 | 185 | name: rickub-${{ steps.check.outputs.version }} |
| 186 | 186 | path: dist/ |
| @@ -189,15 +189,20 @@ jobs: | ||
| 189 | 189 | |
| 190 | 190 | - name: Publish release |
| 191 | 191 | if: steps.check.outputs.dry_run != 'true' |
| 192 | - env: | |
| 193 | - TAG: ${{ steps.check.outputs.tag }} | |
| 194 | - NOTES: ${{ steps.notes.outputs.path }} | |
| 195 | - # The tag does not exist yet: the release shim creates it at --target, | |
| 196 | - # so the dispatched ref's commit is what gets tagged. | |
| 197 | - run: | | |
| 198 | - bash scripts/publish-release.sh \ | |
| 199 | - --tag "${TAG}" --target "${GITHUB_SHA}" --notes "${NOTES}" \ | |
| 200 | - dist/*.tar.gz dist/SHA256SUMS | |
| 192 | + uses: softprops/action-gh-release@v2 | |
| 193 | + with: | |
| 194 | + tag_name: ${{ steps.check.outputs.tag }} | |
| 195 | + name: ${{ steps.check.outputs.tag }} | |
| 196 | + body_path: ${{ steps.notes.outputs.path }} | |
| 197 | + # The tag does not exist yet: the rickub release shim creates it at | |
| 198 | + # target_commitish, so the dispatched ref's commit is what gets tagged. | |
| 199 | + target_commitish: ${{ github.sha }} | |
| 200 | + draft: false | |
| 201 | + prerelease: false | |
| 202 | + files: | | |
| 203 | + dist/*.tar.gz | |
| 204 | + dist/SHA256SUMS | |
| 205 | + fail_on_unmatched_files: true | |
| 201 | 206 | |
| 202 | 207 | - name: Dry run summary |
| 203 | 208 | if: steps.check.outputs.dry_run == 'true' |
| @@ -36,7 +36,7 @@ jobs: | |||
| 36 | runs-on: ubuntu-latest | 36 | runs-on: ubuntu-latest |
| 37 | steps: | 37 | steps: |
| 38 | - name: Checkout | 38 | - name: Checkout |
| 39 | - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | 39 | + uses: actions/checkout@v4 |
| 40 | with: | 40 | with: |
| 41 | # Full history + tags so the "tag already exists" check is meaningful. | 41 | # Full history + tags so the "tag already exists" check is meaningful. |
| 42 | fetch-depth: 0 | 42 | fetch-depth: 0 |
| @@ -133,7 +133,7 @@ jobs: | |||
| 133 | esac | 133 | esac |
| 134 | 134 | ||
| 135 | - name: Set up Go | 135 | - name: Set up Go |
| 136 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | 136 | + uses: actions/setup-go@v5 |
| 137 | with: | 137 | with: |
| 138 | go-version-file: go.mod | 138 | go-version-file: go.mod |
| 139 | cache: true | 139 | cache: true |
| @@ -180,7 +180,7 @@ jobs: | |||
| 180 | echo "path=${RUNNER_TEMP}/release-notes.md" >> "$GITHUB_OUTPUT" | 180 | echo "path=${RUNNER_TEMP}/release-notes.md" >> "$GITHUB_OUTPUT" |
| 181 | 181 | ||
| 182 | - name: Upload build artifacts | 182 | - name: Upload build artifacts |
| 183 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | 183 | + uses: actions/upload-artifact@v4 |
| 184 | with: | 184 | with: |
| 185 | name: rickub-${{ steps.check.outputs.version }} | 185 | name: rickub-${{ steps.check.outputs.version }} |
| 186 | path: dist/ | 186 | path: dist/ |
| @@ -189,15 +189,20 @@ jobs: | |||
| 189 | 189 | ||
| 190 | - name: Publish release | 190 | - name: Publish release |
| 191 | if: steps.check.outputs.dry_run != 'true' | 191 | if: steps.check.outputs.dry_run != 'true' |
| 192 | - env: | 192 | + uses: softprops/action-gh-release@v2 |
| 193 | - TAG: ${{ steps.check.outputs.tag }} | 193 | + with: |
| 194 | - NOTES: ${{ steps.notes.outputs.path }} | 194 | + tag_name: ${{ steps.check.outputs.tag }} |
| 195 | - # The tag does not exist yet: the release shim creates it at --target, | 195 | + name: ${{ steps.check.outputs.tag }} |
| 196 | - # so the dispatched ref's commit is what gets tagged. | 196 | + body_path: ${{ steps.notes.outputs.path }} |
| 197 | - run: | | 197 | + # The tag does not exist yet: the rickub release shim creates it at |
| 198 | - bash scripts/publish-release.sh \ | 198 | + # target_commitish, so the dispatched ref's commit is what gets tagged. |
| 199 | - --tag "${TAG}" --target "${GITHUB_SHA}" --notes "${NOTES}" \ | 199 | + target_commitish: ${{ github.sha }} |
| 200 | - dist/*.tar.gz dist/SHA256SUMS | 200 | + draft: false |
| 201 | + prerelease: false | ||
| 202 | + files: | | ||
| 203 | + dist/*.tar.gz | ||
| 204 | + dist/SHA256SUMS | ||
| 205 | + fail_on_unmatched_files: true | ||
| 201 | 206 | ||
| 202 | - name: Dry run summary | 207 | - name: Dry run summary |
| 203 | if: steps.check.outputs.dry_run == 'true' | 208 | if: steps.check.outputs.dry_run == 'true' |
modified
RELEASING.md +42 -53 | @@ -9,50 +9,40 @@ GitHub-Actions-compatible CI: | ||
| 9 | 9 | | [`.github/workflows/rc.yml`](.github/workflows/rc.yml) | every push to `main` | tests, cross-compiles all four targets, publishes a **prerelease** `v<BASE>-rc.<run_number>` | |
| 10 | 10 | | [`.github/workflows/release.yml`](.github/workflows/release.yml) | **manual only** (`workflow_dispatch`) | tests, cross-compiles, publishes a real release `v<version>` | |
| 11 | 11 | |
| 12 | -Both release-producing workflows call the same two scripts, | |
| 13 | -[`scripts/build-dist.sh`](scripts/build-dist.sh) to build and | |
| 14 | -[`scripts/publish-release.sh`](scripts/publish-release.sh) to publish, so an RC | |
| 15 | -and a real release are byte-for-byte the same pipeline with a different version | |
| 16 | -stamp and the `--prerelease` flag. | |
| 12 | +Both release-producing workflows call the same script, | |
| 13 | +[`scripts/build-dist.sh`](scripts/build-dist.sh), so an RC and a real release | |
| 14 | +are byte-for-byte the same pipeline with a different version stamp. | |
| 17 | 15 | |
| 18 | 16 | --- |
| 19 | 17 | |
| 20 | 18 | ## How the pipeline reaches the release API |
| 21 | 19 | |
| 22 | -Two things about rickub's CI shape this repository's workflows; both are | |
| 23 | -enforced in the files, and both bite as a failed step if forgotten. | |
| 24 | - | |
| 25 | -**`uses:` actions are pinned to commit SHAs, not tags.** The runner fetches | |
| 26 | -every action from github.com. A full-SHA ref is served from the fleet's | |
| 27 | -SHA-keyed action cache with no GitHub round-trip; a tag such as `@v5` first | |
| 28 | -needs an anonymous `ls-remote` against github.com, which GitHub rate-limits per | |
| 29 | -datacenter IP — the run then dies in "Set up Go" with | |
| 30 | -`could not read Username for 'https://github.com'`. The three actions used here | |
| 31 | -(`actions/checkout`, `actions/setup-go`, `actions/upload-artifact`) are pinned | |
| 32 | -to the same SHAs as rickub's own `.rickub/workflows/ci.yml`, so they are always | |
| 33 | -warm in that cache. When bumping one, pin the new SHA and add the `# vN` | |
| 34 | -comment. | |
| 35 | - | |
| 36 | -**Releases are published by [`scripts/publish-release.sh`](scripts/publish-release.sh), | |
| 37 | -not by a marketplace action.** It POSTs the release, then each asset, to the | |
| 38 | -GitHub-compatible REST shim the job is handed as `$GITHUB_API_URL` | |
| 39 | -(`…/gh`), authenticated with the per-job `GITHUB_TOKEN`. Two `curl` calls do | |
| 40 | -not justify fetching `softprops/action-gh-release` (which would be a cold, | |
| 41 | -rate-limited GitHub fetch), and the shim's exact contract — `target_commitish` | |
| 42 | -cuts the tag, `422` on a duplicate release or asset name, `upload_url` | |
| 43 | -template — is handled in one readable place. Both `rc.yml` and `release.yml` | |
| 44 | -call it; a real release and a candidate differ only in `--prerelease`. | |
| 45 | - | |
| 46 | -**Server-side requirement (rickub, not this repo).** The CI guest reaches the | |
| 47 | -control plane only through the node's results proxy (`web/ciproxy` in the | |
| 48 | -rickub server), and `$GITHUB_API_URL` points at that proxy. The proxy must | |
| 49 | -forward the `/gh/` prefix — shipped in rickub PR | |
| 50 | -[ogirardot/rickub#3](https://github.com/ogirardot/rickub/pull/3) and active on a | |
| 20 | +The workflows are written as ordinary GitHub Actions jobs — `actions/checkout@v4`, | |
| 21 | +`actions/setup-go@v5`, `actions/upload-artifact@v4`, | |
| 22 | +`softprops/action-gh-release@v2` — and rickub's CI is expected to run them as | |
| 23 | +such. Two platform facts matter when a run fails before or at publishing: | |
| 24 | + | |
| 25 | +**Fetching `uses:` actions.** rickub's runner fetches every action from | |
| 26 | +github.com. A `@vN` tag is resolved with a `git ls-remote` against github.com; | |
| 27 | +when that call is anonymous, GitHub rate-limits it per datacenter IP and the run | |
| 28 | +dies in "Set up Go" with `could not read Username for 'https://github.com'`. | |
| 29 | +That is a fleet configuration matter, not a workflow one: the runner | |
| 30 | +authenticates those fetches when `RICKUB_ACTIONS_GITHUB_TOKEN` is set | |
| 31 | +(`web/actions/runtime/stage.go` in the rickub server). Do not work around it by | |
| 32 | +pinning SHAs here — the point of this repository's workflows is to be a normal | |
| 33 | +job. | |
| 34 | + | |
| 35 | +**Publishing goes through the node's CI proxy.** The CI guest reaches the | |
| 36 | +control plane only through the results proxy (`web/ciproxy` in the rickub | |
| 37 | +server), and `$GITHUB_API_URL` — what `softprops/action-gh-release` talks to — | |
| 38 | +points at that proxy with the `/gh` REST shim behind it. The proxy must forward | |
| 39 | +the `/gh/` prefix: shipped in rickub PR | |
| 40 | +[ogirardot/rickub#3](https://github.com/ogirardot/rickub/pull/3), active on a | |
| 51 | 41 | node once its `rickub-fleetd` has been rebuilt from that change. On a node |
| 52 | 42 | running an older fleetd, both workflows build, test and upload their |
| 53 | -`actions/upload-artifact` bundle, then fail on the "Publish …" step with | |
| 54 | -`HTTP 403 creating the release`; the archives are still downloadable from the | |
| 55 | -run's artifacts, so a release can be published by hand in the meantime. | |
| 43 | +`actions/upload-artifact` bundle, then fail on the "Publish …" step with a | |
| 44 | +`403`; the archives are still downloadable from the run's artifacts, so a | |
| 45 | +release can be published by hand in the meantime. | |
| 56 | 46 | |
| 57 | 47 | You can rehearse the whole pipeline without publishing by dispatching |
| 58 | 48 | `release.yml` with **`dry_run: true`** — it builds, tests, verifies the tag is |
| @@ -87,13 +77,12 @@ Every push to `main` runs `rc.yml`: | ||
| 87 | 77 | |
| 88 | 78 | 1. `go test ./...` |
| 89 | 79 | 2. `scripts/build-dist.sh <BASE>-rc.<run_number>` |
| 90 | -3. `scripts/publish-release.sh --prerelease …` publishes a **prerelease** at | |
| 91 | - tag `v<BASE>-rc.<run_number>`, with the four `.tar.gz` archives and | |
| 92 | - `SHA256SUMS` attached. | |
| 80 | +3. publishes a **prerelease** at tag `v<BASE>-rc.<run_number>`, with the four | |
| 81 | + `.tar.gz` archives and `SHA256SUMS` attached. | |
| 93 | 82 | |
| 94 | 83 | The tag does not exist beforehand — the release shim creates it at |
| 95 | -`target_commitish`, which the script is given as `${{ github.sha }}`, so CI | |
| 96 | -cuts its own RC tags. | |
| 84 | +`target_commitish`, which the workflow sets to `${{ github.sha }}`, so CI cuts | |
| 85 | +its own RC tags. | |
| 97 | 86 | |
| 98 | 87 | `concurrency: { group: rc, cancel-in-progress: true }` means only the newest |
| 99 | 88 | push to `main` is building at any moment; superseded RC runs are cancelled, so |
| @@ -124,9 +113,9 @@ The workflow then: | ||
| 124 | 113 | non-404 answer is only a warning — the git tag check is the real gate); |
| 125 | 114 | * runs `go vet` and `go test`; |
| 126 | 115 | * builds the four archives + `SHA256SUMS` via `scripts/build-dist.sh`; |
| 127 | -* runs `scripts/publish-release.sh` to publish a non-prerelease release at | |
| 128 | - `v<version>`, targeting `${{ github.sha }}` — the exact commit of the ref you | |
| 129 | - dispatched — so the tag is created at that commit. | |
| 116 | +* publishes a non-prerelease release at `v<version>`, targeting | |
| 117 | + `${{ github.sha }}` — the exact commit of the ref you dispatched — so the tag | |
| 118 | + is created at that commit. | |
| 130 | 119 | |
| 131 | 120 | There is no approval gate: rickub CI silently drops `environment:`, so the |
| 132 | 121 | permission to run this workflow *is* the permission to release. |
| @@ -209,11 +198,12 @@ leaves nothing untracked behind — `dist/` is already in `.gitignore`. | ||
| 209 | 198 | |
| 210 | 199 | ## Troubleshooting |
| 211 | 200 | |
| 212 | -**`HTTP 403 creating the release`.** The node's CI proxy does not forward | |
| 213 | -`/gh/` yet — see "Server-side requirement" above. | |
| 201 | +**`403` publishing the release.** The node's CI proxy does not forward `/gh/` | |
| 202 | +yet — see "Publishing goes through the node's CI proxy" above. | |
| 214 | 203 | |
| 215 | -**`could not read Username for 'https://github.com'` in "Set up Go".** A | |
| 216 | -`uses:` reference is a tag, not a pinned SHA — see the pinning note above. | |
| 204 | +**`could not read Username for 'https://github.com'` in "Set up Go".** The | |
| 205 | +fleet is fetching actions from GitHub anonymously and got rate-limited — see | |
| 206 | +"Fetching `uses:` actions" above; the fix is on the runner, not here. | |
| 217 | 207 | |
| 218 | 208 | **`Resource not accessible by integration` / `404` on release creation.** The |
| 219 | 209 | `GITHUB_TOKEN` is read-only by default; the workflow must declare |
| @@ -224,10 +214,9 @@ or an asset with that name, already exists — the shim rejects duplicates. This | ||
| 224 | 214 | normally means a partially completed run is being retried; delete the release |
| 225 | 215 | (or the asset) and re-run. |
| 226 | 216 | |
| 227 | -**Editing a release via `PATCH`.** The workflows never edit a release, but if | |
| 228 | -you script one: the shim reads `draft` and `prerelease` unconditionally, so | |
| 229 | -**always send both fields** in a PATCH body or you will silently flip a | |
| 230 | -release to draft. | |
| 217 | +**Editing a release via `PATCH`.** The shim reads `draft` and `prerelease` | |
| 218 | +unconditionally, so **always send both fields** in a PATCH body or you will | |
| 219 | +silently flip a release to draft. | |
| 231 | 220 | |
| 232 | 221 | **Nothing to release / `make_latest` ignored.** The shim does not implement |
| 233 | 222 | `make_latest` or `generate_release_notes`, and there is no |
| @@ -9,50 +9,40 @@ GitHub-Actions-compatible CI: | |||
| 9 | | [`.github/workflows/rc.yml`](.github/workflows/rc.yml) | every push to `main` | tests, cross-compiles all four targets, publishes a **prerelease** `v<BASE>-rc.<run_number>` | | 9 | | [`.github/workflows/rc.yml`](.github/workflows/rc.yml) | every push to `main` | tests, cross-compiles all four targets, publishes a **prerelease** `v<BASE>-rc.<run_number>` | |
| 10 | | [`.github/workflows/release.yml`](.github/workflows/release.yml) | **manual only** (`workflow_dispatch`) | tests, cross-compiles, publishes a real release `v<version>` | | 10 | | [`.github/workflows/release.yml`](.github/workflows/release.yml) | **manual only** (`workflow_dispatch`) | tests, cross-compiles, publishes a real release `v<version>` | |
| 11 | 11 | ||
| 12 | -Both release-producing workflows call the same two scripts, | 12 | +Both release-producing workflows call the same script, |
| 13 | -[`scripts/build-dist.sh`](scripts/build-dist.sh) to build and | 13 | +[`scripts/build-dist.sh`](scripts/build-dist.sh), so an RC and a real release |
| 14 | -[`scripts/publish-release.sh`](scripts/publish-release.sh) to publish, so an RC | 14 | +are byte-for-byte the same pipeline with a different version stamp. |
| 15 | -and a real release are byte-for-byte the same pipeline with a different version | ||
| 16 | -stamp and the `--prerelease` flag. | ||
| 17 | 15 | ||
| 18 | --- | 16 | --- |
| 19 | 17 | ||
| 20 | ## How the pipeline reaches the release API | 18 | ## How the pipeline reaches the release API |
| 21 | 19 | ||
| 22 | -Two things about rickub's CI shape this repository's workflows; both are | 20 | +The workflows are written as ordinary GitHub Actions jobs — `actions/checkout@v4`, |
| 23 | -enforced in the files, and both bite as a failed step if forgotten. | 21 | +`actions/setup-go@v5`, `actions/upload-artifact@v4`, |
| 24 | - | 22 | +`softprops/action-gh-release@v2` — and rickub's CI is expected to run them as |
| 25 | -**`uses:` actions are pinned to commit SHAs, not tags.** The runner fetches | 23 | +such. Two platform facts matter when a run fails before or at publishing: |
| 26 | -every action from github.com. A full-SHA ref is served from the fleet's | 24 | + |
| 27 | -SHA-keyed action cache with no GitHub round-trip; a tag such as `@v5` first | 25 | +**Fetching `uses:` actions.** rickub's runner fetches every action from |
| 28 | -needs an anonymous `ls-remote` against github.com, which GitHub rate-limits per | 26 | +github.com. A `@vN` tag is resolved with a `git ls-remote` against github.com; |
| 29 | -datacenter IP — the run then dies in "Set up Go" with | 27 | +when that call is anonymous, GitHub rate-limits it per datacenter IP and the run |
| 30 | -`could not read Username for 'https://github.com'`. The three actions used here | 28 | +dies in "Set up Go" with `could not read Username for 'https://github.com'`. |
| 31 | -(`actions/checkout`, `actions/setup-go`, `actions/upload-artifact`) are pinned | 29 | +That is a fleet configuration matter, not a workflow one: the runner |
| 32 | -to the same SHAs as rickub's own `.rickub/workflows/ci.yml`, so they are always | 30 | +authenticates those fetches when `RICKUB_ACTIONS_GITHUB_TOKEN` is set |
| 33 | -warm in that cache. When bumping one, pin the new SHA and add the `# vN` | 31 | +(`web/actions/runtime/stage.go` in the rickub server). Do not work around it by |
| 34 | -comment. | 32 | +pinning SHAs here — the point of this repository's workflows is to be a normal |
| 35 | - | 33 | +job. |
| 36 | -**Releases are published by [`scripts/publish-release.sh`](scripts/publish-release.sh), | 34 | + |
| 37 | -not by a marketplace action.** It POSTs the release, then each asset, to the | 35 | +**Publishing goes through the node's CI proxy.** The CI guest reaches the |
| 38 | -GitHub-compatible REST shim the job is handed as `$GITHUB_API_URL` | 36 | +control plane only through the results proxy (`web/ciproxy` in the rickub |
| 39 | -(`…/gh`), authenticated with the per-job `GITHUB_TOKEN`. Two `curl` calls do | 37 | +server), and `$GITHUB_API_URL` — what `softprops/action-gh-release` talks to — |
| 40 | -not justify fetching `softprops/action-gh-release` (which would be a cold, | 38 | +points at that proxy with the `/gh` REST shim behind it. The proxy must forward |
| 41 | -rate-limited GitHub fetch), and the shim's exact contract — `target_commitish` | 39 | +the `/gh/` prefix: shipped in rickub PR |
| 42 | -cuts the tag, `422` on a duplicate release or asset name, `upload_url` | 40 | +[ogirardot/rickub#3](https://github.com/ogirardot/rickub/pull/3), active on a |
| 43 | -template — is handled in one readable place. Both `rc.yml` and `release.yml` | ||
| 44 | -call it; a real release and a candidate differ only in `--prerelease`. | ||
| 45 | - | ||
| 46 | -**Server-side requirement (rickub, not this repo).** The CI guest reaches the | ||
| 47 | -control plane only through the node's results proxy (`web/ciproxy` in the | ||
| 48 | -rickub server), and `$GITHUB_API_URL` points at that proxy. The proxy must | ||
| 49 | -forward the `/gh/` prefix — shipped in rickub PR | ||
| 50 | -[ogirardot/rickub#3](https://github.com/ogirardot/rickub/pull/3) and active on a | ||
| 51 | node once its `rickub-fleetd` has been rebuilt from that change. On a node | 41 | node once its `rickub-fleetd` has been rebuilt from that change. On a node |
| 52 | running an older fleetd, both workflows build, test and upload their | 42 | running an older fleetd, both workflows build, test and upload their |
| 53 | -`actions/upload-artifact` bundle, then fail on the "Publish …" step with | 43 | +`actions/upload-artifact` bundle, then fail on the "Publish …" step with a |
| 54 | -`HTTP 403 creating the release`; the archives are still downloadable from the | 44 | +`403`; the archives are still downloadable from the run's artifacts, so a |
| 55 | -run's artifacts, so a release can be published by hand in the meantime. | 45 | +release can be published by hand in the meantime. |
| 56 | 46 | ||
| 57 | You can rehearse the whole pipeline without publishing by dispatching | 47 | You can rehearse the whole pipeline without publishing by dispatching |
| 58 | `release.yml` with **`dry_run: true`** — it builds, tests, verifies the tag is | 48 | `release.yml` with **`dry_run: true`** — it builds, tests, verifies the tag is |
| @@ -87,13 +77,12 @@ Every push to `main` runs `rc.yml`: | |||
| 87 | 77 | ||
| 88 | 1. `go test ./...` | 78 | 1. `go test ./...` |
| 89 | 2. `scripts/build-dist.sh <BASE>-rc.<run_number>` | 79 | 2. `scripts/build-dist.sh <BASE>-rc.<run_number>` |
| 90 | -3. `scripts/publish-release.sh --prerelease …` publishes a **prerelease** at | 80 | +3. publishes a **prerelease** at tag `v<BASE>-rc.<run_number>`, with the four |
| 91 | - tag `v<BASE>-rc.<run_number>`, with the four `.tar.gz` archives and | 81 | + `.tar.gz` archives and `SHA256SUMS` attached. |
| 92 | - `SHA256SUMS` attached. | ||
| 93 | 82 | ||
| 94 | The tag does not exist beforehand — the release shim creates it at | 83 | The tag does not exist beforehand — the release shim creates it at |
| 95 | -`target_commitish`, which the script is given as `${{ github.sha }}`, so CI | 84 | +`target_commitish`, which the workflow sets to `${{ github.sha }}`, so CI cuts |
| 96 | -cuts its own RC tags. | 85 | +its own RC tags. |
| 97 | 86 | ||
| 98 | `concurrency: { group: rc, cancel-in-progress: true }` means only the newest | 87 | `concurrency: { group: rc, cancel-in-progress: true }` means only the newest |
| 99 | push to `main` is building at any moment; superseded RC runs are cancelled, so | 88 | push to `main` is building at any moment; superseded RC runs are cancelled, so |
| @@ -124,9 +113,9 @@ The workflow then: | |||
| 124 | non-404 answer is only a warning — the git tag check is the real gate); | 113 | non-404 answer is only a warning — the git tag check is the real gate); |
| 125 | * runs `go vet` and `go test`; | 114 | * runs `go vet` and `go test`; |
| 126 | * builds the four archives + `SHA256SUMS` via `scripts/build-dist.sh`; | 115 | * builds the four archives + `SHA256SUMS` via `scripts/build-dist.sh`; |
| 127 | -* runs `scripts/publish-release.sh` to publish a non-prerelease release at | 116 | +* publishes a non-prerelease release at `v<version>`, targeting |
| 128 | - `v<version>`, targeting `${{ github.sha }}` — the exact commit of the ref you | 117 | + `${{ github.sha }}` — the exact commit of the ref you dispatched — so the tag |
| 129 | - dispatched — so the tag is created at that commit. | 118 | + is created at that commit. |
| 130 | 119 | ||
| 131 | There is no approval gate: rickub CI silently drops `environment:`, so the | 120 | There is no approval gate: rickub CI silently drops `environment:`, so the |
| 132 | permission to run this workflow *is* the permission to release. | 121 | permission to run this workflow *is* the permission to release. |
| @@ -209,11 +198,12 @@ leaves nothing untracked behind — `dist/` is already in `.gitignore`. | |||
| 209 | 198 | ||
| 210 | ## Troubleshooting | 199 | ## Troubleshooting |
| 211 | 200 | ||
| 212 | -**`HTTP 403 creating the release`.** The node's CI proxy does not forward | 201 | +**`403` publishing the release.** The node's CI proxy does not forward `/gh/` |
| 213 | -`/gh/` yet — see "Server-side requirement" above. | 202 | +yet — see "Publishing goes through the node's CI proxy" above. |
| 214 | 203 | ||
| 215 | -**`could not read Username for 'https://github.com'` in "Set up Go".** A | 204 | +**`could not read Username for 'https://github.com'` in "Set up Go".** The |
| 216 | -`uses:` reference is a tag, not a pinned SHA — see the pinning note above. | 205 | +fleet is fetching actions from GitHub anonymously and got rate-limited — see |
| 206 | +"Fetching `uses:` actions" above; the fix is on the runner, not here. | ||
| 217 | 207 | ||
| 218 | **`Resource not accessible by integration` / `404` on release creation.** The | 208 | **`Resource not accessible by integration` / `404` on release creation.** The |
| 219 | `GITHUB_TOKEN` is read-only by default; the workflow must declare | 209 | `GITHUB_TOKEN` is read-only by default; the workflow must declare |
| @@ -224,10 +214,9 @@ or an asset with that name, already exists — the shim rejects duplicates. This | |||
| 224 | normally means a partially completed run is being retried; delete the release | 214 | normally means a partially completed run is being retried; delete the release |
| 225 | (or the asset) and re-run. | 215 | (or the asset) and re-run. |
| 226 | 216 | ||
| 227 | -**Editing a release via `PATCH`.** The workflows never edit a release, but if | 217 | +**Editing a release via `PATCH`.** The shim reads `draft` and `prerelease` |
| 228 | -you script one: the shim reads `draft` and `prerelease` unconditionally, so | 218 | +unconditionally, so **always send both fields** in a PATCH body or you will |
| 229 | -**always send both fields** in a PATCH body or you will silently flip a | 219 | +silently flip a release to draft. |
| 230 | -release to draft. | ||
| 231 | 220 | ||
| 232 | **Nothing to release / `make_latest` ignored.** The shim does not implement | 221 | **Nothing to release / `make_latest` ignored.** The shim does not implement |
| 233 | `make_latest` or `generate_release_notes`, and there is no | 222 | `make_latest` or `generate_release_notes`, and there is no |
deleted
scripts/publish-release.sh +0 -110 | deleted file mode 100755 | ||
| @@ -1,110 +0,0 @@ | ||
| 1 | -#!/usr/bin/env bash | |
| 2 | -# | |
| 3 | -# publish-release.sh — create a release on the rickub host and attach assets, | |
| 4 | -# straight against the GitHub-compatible REST shim the CI job is handed. | |
| 5 | -# | |
| 6 | -# Usage: scripts/publish-release.sh --tag v1.2.3 --target <sha> --notes notes.md \ | |
| 7 | -# [--name v1.2.3] [--prerelease] [--draft] file... | |
| 8 | -# | |
| 9 | -# Environment (all injected by rickub CI; set them by hand to run elsewhere): | |
| 10 | -# GITHUB_API_URL shim base, e.g. http://172.30.255.1:8888/gh | |
| 11 | -# GITHUB_TOKEN the per-job token (contents: write) | |
| 12 | -# GITHUB_REPOSITORY owner/repo | |
| 13 | -# | |
| 14 | -# Why a script and not softprops/action-gh-release: rickub's runner fetches every | |
| 15 | -# `uses:` action from github.com, and an action that is not already in the fleet's | |
| 16 | -# SHA-keyed cache costs an anonymous GitHub fetch that github.com rate-limits per | |
| 17 | -# datacenter IP. The two calls the release needs — POST the release, POST each | |
| 18 | -# asset — are trivial with curl, and this way the release pipeline depends on | |
| 19 | -# nothing that has to be fetched at run time. It needs curl and jq, both present | |
| 20 | -# in the CI guest. | |
| 21 | - | |
| 22 | -set -euo pipefail | |
| 23 | - | |
| 24 | -usage() { sed -n '2,20p' "${BASH_SOURCE[0]}" >&2; exit 2; } | |
| 25 | - | |
| 26 | -tag=""; target=""; notes=""; name=""; prerelease=false; draft=false | |
| 27 | -files=() | |
| 28 | -while [ $# -gt 0 ]; do | |
| 29 | - case "$1" in | |
| 30 | - --tag) tag="$2"; shift 2 ;; | |
| 31 | - --target) target="$2"; shift 2 ;; | |
| 32 | - --notes) notes="$2"; shift 2 ;; | |
| 33 | - --name) name="$2"; shift 2 ;; | |
| 34 | - --prerelease) prerelease=true; shift ;; | |
| 35 | - --draft) draft=true; shift ;; | |
| 36 | - -h|--help) usage ;; | |
| 37 | - --) shift; files+=("$@"); break ;; | |
| 38 | - -*) echo "publish-release.sh: unknown flag $1" >&2; usage ;; | |
| 39 | - *) files+=("$1"); shift ;; | |
| 40 | - esac | |
| 41 | -done | |
| 42 | - | |
| 43 | -[ -n "${tag}" ] || { echo "publish-release.sh: --tag is required" >&2; exit 2; } | |
| 44 | -[ -n "${notes}" ] && [ -f "${notes}" ] || { echo "publish-release.sh: --notes must name a file" >&2; exit 2; } | |
| 45 | -[ "${#files[@]}" -gt 0 ] || { echo "publish-release.sh: no asset files given" >&2; exit 2; } | |
| 46 | -for f in "${files[@]}"; do | |
| 47 | - [ -f "${f}" ] || { echo "publish-release.sh: asset not found: ${f}" >&2; exit 2; } | |
| 48 | -done | |
| 49 | -for v in GITHUB_API_URL GITHUB_TOKEN GITHUB_REPOSITORY; do | |
| 50 | - [ -n "${!v:-}" ] || { echo "publish-release.sh: ${v} is not set" >&2; exit 2; } | |
| 51 | -done | |
| 52 | -command -v curl >/dev/null || { echo "publish-release.sh: curl not found" >&2; exit 2; } | |
| 53 | -command -v jq >/dev/null || { echo "publish-release.sh: jq not found" >&2; exit 2; } | |
| 54 | - | |
| 55 | -api="${GITHUB_API_URL%/}" | |
| 56 | -name="${name:-${tag}}" | |
| 57 | - | |
| 58 | -# Never echo the token: -H is passed through a config file, not the command line | |
| 59 | -# (the guest's own process list is untrusted-code territory). | |
| 60 | -auth_cfg="$(mktemp)" | |
| 61 | -trap 'rm -f "${auth_cfg}"' EXIT | |
| 62 | -printf 'header = "Authorization: Bearer %s"\n' "${GITHUB_TOKEN}" > "${auth_cfg}" | |
| 63 | - | |
| 64 | -body="$(mktemp)" | |
| 65 | -trap 'rm -f "${auth_cfg}" "${body}"' EXIT | |
| 66 | - | |
| 67 | -# request METHOD URL [curl args...]: the response body lands in ${body}, the | |
| 68 | -# HTTP status is printed (000 when curl itself failed). | |
| 69 | -request() { | |
| 70 | - local method="$1" url="$2"; shift 2 | |
| 71 | - curl -sS -o "${body}" -w '%{http_code}' -K "${auth_cfg}" \ | |
| 72 | - -H 'Accept: application/vnd.github+json' -X "${method}" "$@" "${url}" || true | |
| 73 | -} | |
| 74 | -message() { jq -r '.message // .' "${body}" 2>/dev/null || cat "${body}"; } | |
| 75 | - | |
| 76 | -echo "==> creating release ${tag} on ${GITHUB_REPOSITORY} (prerelease=${prerelease}, draft=${draft})" | |
| 77 | -payload="$(jq -n \ | |
| 78 | - --arg tag "${tag}" --arg target "${target}" --arg name "${name}" \ | |
| 79 | - --rawfile body "${notes}" --argjson prerelease "${prerelease}" --argjson draft "${draft}" \ | |
| 80 | - '{tag_name: $tag, name: $name, body: $body, draft: $draft, prerelease: $prerelease} | |
| 81 | - + (if $target == "" then {} else {target_commitish: $target} end)')" | |
| 82 | -status="$(request POST "${api}/repos/${GITHUB_REPOSITORY}/releases" \ | |
| 83 | - -H 'Content-Type: application/json' --data-binary "${payload}")" | |
| 84 | -case "${status}" in | |
| 85 | - 200|201) ;; | |
| 86 | - 422) echo "::error::release ${tag} was refused (HTTP 422): $(message)"; exit 1 ;; | |
| 87 | - 403) echo "::error::HTTP 403 creating the release — the node's CI proxy does not forward ${api}; see RELEASING.md"; exit 1 ;; | |
| 88 | - *) echo "::error::creating release ${tag} failed (HTTP ${status}): $(message)"; exit 1 ;; | |
| 89 | -esac | |
| 90 | -release_id="$(jq -r '.id // empty' "${body}")" | |
| 91 | -upload_url="$(jq -r '.upload_url // empty' "${body}" | sed 's/{[^}]*}$//')" | |
| 92 | -html_url="$(jq -r '.html_url // empty' "${body}")" | |
| 93 | -[ -n "${release_id}" ] || { echo "::error::release response carried no id: $(cat "${body}")"; exit 1; } | |
| 94 | -[ -n "${upload_url}" ] || { echo "::error::release response carried no upload_url: $(cat "${body}")"; exit 1; } | |
| 95 | -echo " release id ${release_id}${html_url:+ — ${html_url}}" | |
| 96 | - | |
| 97 | -for f in "${files[@]}"; do | |
| 98 | - asset="$(basename "${f}")" | |
| 99 | - echo "==> uploading ${asset} ($(wc -c < "${f}" | tr -d ' ') bytes)" | |
| 100 | - encoded="$(jq -rn --arg n "${asset}" '$n|@uri')" | |
| 101 | - status="$(request POST "${upload_url}?name=${encoded}" \ | |
| 102 | - -H 'Content-Type: application/octet-stream' --data-binary "@${f}")" | |
| 103 | - case "${status}" in | |
| 104 | - 200|201) ;; | |
| 105 | - 422) echo "::error::asset ${asset} was refused (HTTP 422): $(message) — a partial earlier run? delete the release and retry"; exit 1 ;; | |
| 106 | - *) echo "::error::uploading ${asset} failed (HTTP ${status}): $(message)"; exit 1 ;; | |
| 107 | - esac | |
| 108 | -done | |
| 109 | - | |
| 110 | -echo "==> published ${tag} with ${#files[@]} asset(s)" | |
| deleted file mode 100755 | |||
| @@ -1,110 +0,0 @@ | |||
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# | ||
| 3 | -# publish-release.sh — create a release on the rickub host and attach assets, | ||
| 4 | -# straight against the GitHub-compatible REST shim the CI job is handed. | ||
| 5 | -# | ||
| 6 | -# Usage: scripts/publish-release.sh --tag v1.2.3 --target <sha> --notes notes.md \ | ||
| 7 | -# [--name v1.2.3] [--prerelease] [--draft] file... | ||
| 8 | -# | ||
| 9 | -# Environment (all injected by rickub CI; set them by hand to run elsewhere): | ||
| 10 | -# GITHUB_API_URL shim base, e.g. http://172.30.255.1:8888/gh | ||
| 11 | -# GITHUB_TOKEN the per-job token (contents: write) | ||
| 12 | -# GITHUB_REPOSITORY owner/repo | ||
| 13 | -# | ||
| 14 | -# Why a script and not softprops/action-gh-release: rickub's runner fetches every | ||
| 15 | -# `uses:` action from github.com, and an action that is not already in the fleet's | ||
| 16 | -# SHA-keyed cache costs an anonymous GitHub fetch that github.com rate-limits per | ||
| 17 | -# datacenter IP. The two calls the release needs — POST the release, POST each | ||
| 18 | -# asset — are trivial with curl, and this way the release pipeline depends on | ||
| 19 | -# nothing that has to be fetched at run time. It needs curl and jq, both present | ||
| 20 | -# in the CI guest. | ||
| 21 | - | ||
| 22 | -set -euo pipefail | ||
| 23 | - | ||
| 24 | -usage() { sed -n '2,20p' "${BASH_SOURCE[0]}" >&2; exit 2; } | ||
| 25 | - | ||
| 26 | -tag=""; target=""; notes=""; name=""; prerelease=false; draft=false | ||
| 27 | -files=() | ||
| 28 | -while [ $# -gt 0 ]; do | ||
| 29 | - case "$1" in | ||
| 30 | - --tag) tag="$2"; shift 2 ;; | ||
| 31 | - --target) target="$2"; shift 2 ;; | ||
| 32 | - --notes) notes="$2"; shift 2 ;; | ||
| 33 | - --name) name="$2"; shift 2 ;; | ||
| 34 | - --prerelease) prerelease=true; shift ;; | ||
| 35 | - --draft) draft=true; shift ;; | ||
| 36 | - -h|--help) usage ;; | ||
| 37 | - --) shift; files+=("$@"); break ;; | ||
| 38 | - -*) echo "publish-release.sh: unknown flag $1" >&2; usage ;; | ||
| 39 | - *) files+=("$1"); shift ;; | ||
| 40 | - esac | ||
| 41 | -done | ||
| 42 | - | ||
| 43 | -[ -n "${tag}" ] || { echo "publish-release.sh: --tag is required" >&2; exit 2; } | ||
| 44 | -[ -n "${notes}" ] && [ -f "${notes}" ] || { echo "publish-release.sh: --notes must name a file" >&2; exit 2; } | ||
| 45 | -[ "${#files[@]}" -gt 0 ] || { echo "publish-release.sh: no asset files given" >&2; exit 2; } | ||
| 46 | -for f in "${files[@]}"; do | ||
| 47 | - [ -f "${f}" ] || { echo "publish-release.sh: asset not found: ${f}" >&2; exit 2; } | ||
| 48 | -done | ||
| 49 | -for v in GITHUB_API_URL GITHUB_TOKEN GITHUB_REPOSITORY; do | ||
| 50 | - [ -n "${!v:-}" ] || { echo "publish-release.sh: ${v} is not set" >&2; exit 2; } | ||
| 51 | -done | ||
| 52 | -command -v curl >/dev/null || { echo "publish-release.sh: curl not found" >&2; exit 2; } | ||
| 53 | -command -v jq >/dev/null || { echo "publish-release.sh: jq not found" >&2; exit 2; } | ||
| 54 | - | ||
| 55 | -api="${GITHUB_API_URL%/}" | ||
| 56 | -name="${name:-${tag}}" | ||
| 57 | - | ||
| 58 | -# Never echo the token: -H is passed through a config file, not the command line | ||
| 59 | -# (the guest's own process list is untrusted-code territory). | ||
| 60 | -auth_cfg="$(mktemp)" | ||
| 61 | -trap 'rm -f "${auth_cfg}"' EXIT | ||
| 62 | -printf 'header = "Authorization: Bearer %s"\n' "${GITHUB_TOKEN}" > "${auth_cfg}" | ||
| 63 | - | ||
| 64 | -body="$(mktemp)" | ||
| 65 | -trap 'rm -f "${auth_cfg}" "${body}"' EXIT | ||
| 66 | - | ||
| 67 | -# request METHOD URL [curl args...]: the response body lands in ${body}, the | ||
| 68 | -# HTTP status is printed (000 when curl itself failed). | ||
| 69 | -request() { | ||
| 70 | - local method="$1" url="$2"; shift 2 | ||
| 71 | - curl -sS -o "${body}" -w '%{http_code}' -K "${auth_cfg}" \ | ||
| 72 | - -H 'Accept: application/vnd.github+json' -X "${method}" "$@" "${url}" || true | ||
| 73 | -} | ||
| 74 | -message() { jq -r '.message // .' "${body}" 2>/dev/null || cat "${body}"; } | ||
| 75 | - | ||
| 76 | -echo "==> creating release ${tag} on ${GITHUB_REPOSITORY} (prerelease=${prerelease}, draft=${draft})" | ||
| 77 | -payload="$(jq -n \ | ||
| 78 | - --arg tag "${tag}" --arg target "${target}" --arg name "${name}" \ | ||
| 79 | - --rawfile body "${notes}" --argjson prerelease "${prerelease}" --argjson draft "${draft}" \ | ||
| 80 | - '{tag_name: $tag, name: $name, body: $body, draft: $draft, prerelease: $prerelease} | ||
| 81 | - + (if $target == "" then {} else {target_commitish: $target} end)')" | ||
| 82 | -status="$(request POST "${api}/repos/${GITHUB_REPOSITORY}/releases" \ | ||
| 83 | - -H 'Content-Type: application/json' --data-binary "${payload}")" | ||
| 84 | -case "${status}" in | ||
| 85 | - 200|201) ;; | ||
| 86 | - 422) echo "::error::release ${tag} was refused (HTTP 422): $(message)"; exit 1 ;; | ||
| 87 | - 403) echo "::error::HTTP 403 creating the release — the node's CI proxy does not forward ${api}; see RELEASING.md"; exit 1 ;; | ||
| 88 | - *) echo "::error::creating release ${tag} failed (HTTP ${status}): $(message)"; exit 1 ;; | ||
| 89 | -esac | ||
| 90 | -release_id="$(jq -r '.id // empty' "${body}")" | ||
| 91 | -upload_url="$(jq -r '.upload_url // empty' "${body}" | sed 's/{[^}]*}$//')" | ||
| 92 | -html_url="$(jq -r '.html_url // empty' "${body}")" | ||
| 93 | -[ -n "${release_id}" ] || { echo "::error::release response carried no id: $(cat "${body}")"; exit 1; } | ||
| 94 | -[ -n "${upload_url}" ] || { echo "::error::release response carried no upload_url: $(cat "${body}")"; exit 1; } | ||
| 95 | -echo " release id ${release_id}${html_url:+ — ${html_url}}" | ||
| 96 | - | ||
| 97 | -for f in "${files[@]}"; do | ||
| 98 | - asset="$(basename "${f}")" | ||
| 99 | - echo "==> uploading ${asset} ($(wc -c < "${f}" | tr -d ' ') bytes)" | ||
| 100 | - encoded="$(jq -rn --arg n "${asset}" '$n|@uri')" | ||
| 101 | - status="$(request POST "${upload_url}?name=${encoded}" \ | ||
| 102 | - -H 'Content-Type: application/octet-stream' --data-binary "@${f}")" | ||
| 103 | - case "${status}" in | ||
| 104 | - 200|201) ;; | ||
| 105 | - 422) echo "::error::asset ${asset} was refused (HTTP 422): $(message) — a partial earlier run? delete the release and retry"; exit 1 ;; | ||
| 106 | - *) echo "::error::uploading ${asset} failed (HTTP ${status}): $(message)"; exit 1 ;; | ||
| 107 | - esac | ||
| 108 | -done | ||
| 109 | - | ||
| 110 | -echo "==> published ${tag} with ${#files[@]} asset(s)" | ||