From aeb5539f59a0f2620ebcaf333375c612b5541739 Mon Sep 17 00:00:00 2001 From: Altay Date: Fri, 8 May 2026 20:37:12 +0300 Subject: [PATCH 1/6] fix(ci): harden release supply chain --- .github/workflows/backfill-release-assets.yml | 77 +++++++++++----- .github/workflows/ci.yml | 86 +++++++++++------- package.json | 2 +- scripts/{build-sea.mjs => build-sea.mts} | 91 +++++++++++++++++-- 4 files changed, 192 insertions(+), 64 deletions(-) rename scripts/{build-sea.mjs => build-sea.mts} (65%) diff --git a/.github/workflows/backfill-release-assets.yml b/.github/workflows/backfill-release-assets.yml index e47fa3b..14df7f2 100644 --- a/.github/workflows/backfill-release-assets.yml +++ b/.github/workflows/backfill-release-assets.yml @@ -9,8 +9,27 @@ on: type: string jobs: + validate-release-tag: + name: Validate release tag + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + + steps: + - name: Validate tag name + env: + TAG_NAME: ${{ inputs.tag_name }} + run: | + if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid release tag: $TAG_NAME" >&2 + exit 1 + fi + build-unix-binaries: name: Build ${{ matrix.os }} release assets + needs: + - validate-release-tag runs-on: ${{ matrix.os }} timeout-minutes: 30 permissions: @@ -28,29 +47,35 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 + ref: ${{ inputs.tag_name }} - - name: Check out release tag - run: git checkout ${{ inputs.tag_name }} - - - name: Set up Vite+ - uses: voidzero-dev/setup-vp@v1 + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: true + cache: pnpm + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Build SEA binary - run: vp run build:sea + run: pnpm run build:sea - name: Verify SEA binary - run: vp run verify:sea + run: pnpm run verify:sea - name: Package release assets shell: pwsh + env: + TAG_NAME: ${{ inputs.tag_name }} run: | - $version = "${{ inputs.tag_name }}".TrimStart("v") + $version = $env:TAG_NAME.TrimStart("v") $assetBase = "putio-cli-$version-${{ matrix.asset_os }}-${{ matrix.asset_arch }}" $releaseDir = ".artifacts/release" New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null @@ -70,7 +95,7 @@ jobs: run: Get-ChildItem .artifacts/release - name: Upload binary assets to the GitHub release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: tag_name: ${{ inputs.tag_name }} files: | @@ -78,6 +103,8 @@ jobs: build-windows-binary: name: Build windows-latest release assets + needs: + - validate-release-tag runs-on: windows-latest timeout-minutes: 30 permissions: @@ -85,29 +112,35 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 + ref: ${{ inputs.tag_name }} - - name: Check out release tag - run: git checkout ${{ inputs.tag_name }} - - - name: Set up Vite+ - uses: voidzero-dev/setup-vp@v1 + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: true + cache: pnpm + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Build SEA binary - run: vp run build:sea + run: pnpm run build:sea - name: Verify SEA binary - run: vp run verify:sea + run: pnpm run verify:sea - name: Package release assets shell: pwsh + env: + TAG_NAME: ${{ inputs.tag_name }} run: | - $version = "${{ inputs.tag_name }}".TrimStart("v") + $version = $env:TAG_NAME.TrimStart("v") $assetBase = "putio-cli-$version-windows-amd64" $releaseDir = ".artifacts/release" New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null @@ -122,7 +155,7 @@ jobs: run: Get-ChildItem .artifacts/release - name: Upload binary assets to the GitHub release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: tag_name: ${{ inputs.tag_name }} files: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 120daf1..e620419 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,21 +21,27 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - - name: Set up Vite+ - uses: voidzero-dev/setup-vp@v1 + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: true + cache: pnpm + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Verify repository - run: vp run verify + run: pnpm run verify - name: Smoke test packed install surface - run: vp run smoke:pack + run: pnpm run smoke:pack release: if: github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]') @@ -55,22 +61,28 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - - name: Set up Vite+ - uses: voidzero-dev/setup-vp@v1 + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: true + cache: pnpm + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Build package - run: vp pack + run: pnpm run build - name: Release package id: semantic - uses: cycjimmy/semantic-release-action@v6 + uses: cycjimmy/semantic-release-action@b12c8f6015dc215fe37bc154d4ad456dd3833c90 # v6.0.0 with: extra_plugins: | @semantic-release/commit-analyzer @@ -109,24 +121,28 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 + ref: ${{ needs.release.outputs.new_release_git_tag }} - - name: Check out release tag - run: git checkout ${{ needs.release.outputs.new_release_git_tag }} - - - name: Set up Vite+ - uses: voidzero-dev/setup-vp@v1 + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: true + cache: pnpm + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Build SEA binary - run: vp run build:sea + run: pnpm run build:sea - name: Verify SEA binary - run: vp run verify:sea + run: pnpm run verify:sea - name: Package release assets shell: pwsh @@ -151,7 +167,7 @@ jobs: run: Get-ChildItem .artifacts/release - name: Upload binary assets to the GitHub release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: tag_name: ${{ needs.release.outputs.new_release_git_tag }} files: | @@ -169,24 +185,28 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 + ref: ${{ needs.release.outputs.new_release_git_tag }} - - name: Check out release tag - run: git checkout ${{ needs.release.outputs.new_release_git_tag }} - - - name: Set up Vite+ - uses: voidzero-dev/setup-vp@v1 + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: true + cache: pnpm + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Build SEA binary - run: vp run build:sea + run: pnpm run build:sea - name: Verify SEA binary - run: vp run verify:sea + run: pnpm run verify:sea - name: Package release assets shell: pwsh @@ -206,7 +226,7 @@ jobs: run: Get-ChildItem .artifacts/release - name: Upload binary assets to the GitHub release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: tag_name: ${{ needs.release.outputs.new_release_git_tag }} files: | @@ -225,7 +245,7 @@ jobs: steps: - name: Release to Homebrew tap - uses: Justintime50/homebrew-releaser@v3 + uses: Justintime50/homebrew-releaser@a62d7a359683bfc047cdb2431f53ee58241464d1 # v3 with: homebrew_owner: putdotio homebrew_tap: homebrew-tap diff --git a/package.json b/package.json index 4b02b08..2965575 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ }, "scripts": { "build": "vp pack", - "build:sea": "node ./scripts/build-sea.mjs", + "build:sea": "node ./scripts/build-sea.mts", "check": "vp check .", "coverage": "vp test --coverage", "dev": "vp pack --watch", diff --git a/scripts/build-sea.mjs b/scripts/build-sea.mts similarity index 65% rename from scripts/build-sea.mjs rename to scripts/build-sea.mts index 4dd2b03..80f7ef0 100644 --- a/scripts/build-sea.mjs +++ b/scripts/build-sea.mts @@ -1,5 +1,14 @@ +import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; -import { createWriteStream, cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { + createWriteStream, + cpSync, + existsSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { mkdir, unlink } from "node:fs/promises"; import { request } from "node:https"; import { dirname, join } from "node:path"; @@ -22,7 +31,14 @@ const seaSentinelFuse = "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2"; const localBin = (name) => join(root, "node_modules", ".bin", `${name}${platform === "win32" ? ".cmd" : ""}`); -const run = (command, args, options = {}) => { +type RunOptions = { + readonly cwd?: string; + readonly encoding?: BufferEncoding; + readonly stdio?: "inherit"; + readonly windowsVerbatimArguments?: boolean; +}; + +const run = (command: string, args: ReadonlyArray, options: RunOptions = {}) => { if (platform === "win32" && /\.(cmd|bat)$/i.test(command)) { return execFileSync(process.env.comspec ?? "cmd.exe", ["/d", "/s", "/c", command, ...args], { cwd: root, @@ -41,10 +57,10 @@ const run = (command, args, options = {}) => { }); }; -const downloadFile = async (url, destination) => { +const downloadFile = async (url: string, destination: string): Promise => { await mkdir(dirname(destination), { recursive: true }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const req = request(url, (response) => { if ( response.statusCode && @@ -62,7 +78,7 @@ const downloadFile = async (url, destination) => { return; } - pipeline(response, createWriteStream(destination)).then(resolve, reject); + void pipeline(response, createWriteStream(destination)).then(resolve, reject); }); req.on("error", reject); @@ -70,6 +86,53 @@ const downloadFile = async (url, destination) => { }); }; +const downloadText = async (url: string): Promise => + await new Promise((resolve, reject) => { + const req = request(url, (response) => { + if ( + response.statusCode && + response.statusCode >= 300 && + response.statusCode < 400 && + typeof response.headers.location === "string" + ) { + response.resume(); + resolve(downloadText(response.headers.location)); + return; + } + + if (response.statusCode !== 200) { + reject(new Error(`Unable to download ${url}. Received status ${response.statusCode}.`)); + return; + } + + let text = ""; + response.setEncoding("utf8"); + response.on("data", (chunk: string) => { + text += chunk; + }); + response.on("end", () => resolve(text)); + response.on("error", reject); + }); + + req.on("error", reject); + req.end(); + }); + +const sha256File = (filePath: string) => + createHash("sha256").update(readFileSync(filePath)).digest("hex"); + +const resolveExpectedChecksum = (shasums: string, archiveName: string) => { + for (const line of shasums.split(/\r?\n/u)) { + const [checksum, filename] = line.trim().split(/\s+/u); + + if (checksum !== undefined && filename === archiveName) { + return checksum; + } + } + + throw new Error(`Unable to resolve checksum for ${archiveName}.`); +}; + const resolveOfficialNodeRuntime = async () => { if (process.env.SEA_NODE_BINARY) { return process.env.SEA_NODE_BINARY; @@ -88,10 +151,22 @@ const resolveOfficialNodeRuntime = async () => { return nodeBinary; } - await downloadFile( - `https://nodejs.org/dist/v${version}/${baseName}.${archiveExtension}`, - archivePath, + const archiveName = `${baseName}.${archiveExtension}`; + const nodeDistBase = `https://nodejs.org/dist/v${version}`; + + await downloadFile(`${nodeDistBase}/${archiveName}`, archivePath); + + const expectedChecksum = resolveExpectedChecksum( + await downloadText(`${nodeDistBase}/SHASUMS256.txt`), + archiveName, ); + const actualChecksum = sha256File(archivePath); + + if (actualChecksum.toLowerCase() !== expectedChecksum.toLowerCase()) { + throw new Error( + `Checksum mismatch for ${archiveName}. Expected ${expectedChecksum}, received ${actualChecksum}.`, + ); + } mkdirSync(runtimeDir, { recursive: true }); if (platform === "win32") { From b4804de0fd690214bd2cd6fbe8c60935c1644e46 Mon Sep 17 00:00:00 2001 From: Altay Date: Fri, 8 May 2026 21:15:19 +0300 Subject: [PATCH 2/6] fix(ci): avoid pnpm cache bootstrap failure --- .github/workflows/backfill-release-assets.yml | 2 -- .github/workflows/ci.yml | 4 ---- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/backfill-release-assets.yml b/.github/workflows/backfill-release-assets.yml index 14df7f2..aa232bd 100644 --- a/.github/workflows/backfill-release-assets.yml +++ b/.github/workflows/backfill-release-assets.yml @@ -56,7 +56,6 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: pnpm - name: Enable Corepack run: corepack enable @@ -121,7 +120,6 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: pnpm - name: Enable Corepack run: corepack enable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e620419..ea0bc64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: pnpm - name: Enable Corepack run: corepack enable @@ -69,7 +68,6 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: pnpm - name: Enable Corepack run: corepack enable @@ -130,7 +128,6 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: pnpm - name: Enable Corepack run: corepack enable @@ -194,7 +191,6 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: ".node-version" - cache: pnpm - name: Enable Corepack run: corepack enable From dac2f9d5a88f8439fe1a27be3ea56727aed24ef3 Mon Sep 17 00:00:00 2001 From: Altay Date: Fri, 8 May 2026 22:18:20 +0300 Subject: [PATCH 3/6] docs: use titled routing links --- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 471efaf..220c2b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ Runtime proofs: - Prefer `Effect`, services, layers, `Schema`, and tagged errors over ad hoc control flow. - Treat JSON output as the machine contract and terminal output as a separate adapter layer. - Update docs when flags, command behavior, or architecture boundaries change. -- When the public CLI surface or agent-facing setup flow changes, update [`README.md`](README.md) and [`skills/putio-cli/SKILL.md`](skills/putio-cli/SKILL.md) together so the copy-paste prompt and consumer guidance stay aligned. +- When the public CLI surface or agent-facing setup flow changes, update [Overview](README.md) and [CLI skill](skills/putio-cli/SKILL.md) together so the copy-paste prompt and consumer guidance stay aligned. - Do not hardcode volatile metrics in docs. ## Testing diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc55467..1a81dc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for contributing to `putio-cli`. ## Setup -Use the Node version required by [`package.json`](./package.json), then install dependencies: +Use the Node version required by [package metadata](./package.json), then install dependencies: ```bash vp install diff --git a/README.md b/README.md index d9f01bd..e90b2e1 100644 --- a/README.md +++ b/README.md @@ -135,4 +135,4 @@ putio transfers list --page-all --output ndjson ## License -This project is available under the MIT license. See [LICENSE](./LICENSE). +This project is available under the MIT license. See [License](./LICENSE). From 1a4d9f5f8854ba6693af240f0c1781611c5e0985 Mon Sep 17 00:00:00 2001 From: Altay Date: Fri, 8 May 2026 22:25:50 +0300 Subject: [PATCH 4/6] chore(docs): trim unrelated link label changes --- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 220c2b1..471efaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ Runtime proofs: - Prefer `Effect`, services, layers, `Schema`, and tagged errors over ad hoc control flow. - Treat JSON output as the machine contract and terminal output as a separate adapter layer. - Update docs when flags, command behavior, or architecture boundaries change. -- When the public CLI surface or agent-facing setup flow changes, update [Overview](README.md) and [CLI skill](skills/putio-cli/SKILL.md) together so the copy-paste prompt and consumer guidance stay aligned. +- When the public CLI surface or agent-facing setup flow changes, update [`README.md`](README.md) and [`skills/putio-cli/SKILL.md`](skills/putio-cli/SKILL.md) together so the copy-paste prompt and consumer guidance stay aligned. - Do not hardcode volatile metrics in docs. ## Testing diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a81dc5..fc55467 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for contributing to `putio-cli`. ## Setup -Use the Node version required by [package metadata](./package.json), then install dependencies: +Use the Node version required by [`package.json`](./package.json), then install dependencies: ```bash vp install diff --git a/README.md b/README.md index e90b2e1..d9f01bd 100644 --- a/README.md +++ b/README.md @@ -135,4 +135,4 @@ putio transfers list --page-all --output ndjson ## License -This project is available under the MIT license. See [License](./LICENSE). +This project is available under the MIT license. See [LICENSE](./LICENSE). From ef19a653fa7d26e33e86b6c9fcd6442b53860995 Mon Sep 17 00:00:00 2001 From: Altay Date: Fri, 8 May 2026 23:01:16 +0300 Subject: [PATCH 5/6] fix(ci): use release environment for publish secrets --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea0bc64..d23ed9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: - verify runs-on: ubuntu-latest timeout-minutes: 20 + environment: release permissions: contents: write issues: write @@ -236,6 +237,7 @@ jobs: - build-unix-binaries runs-on: ubuntu-latest timeout-minutes: 20 + environment: release permissions: contents: read From dccc8e572ef5408e42684d53ee803894affd5e7b Mon Sep 17 00:00:00 2001 From: Altay Date: Sat, 9 May 2026 00:24:33 +0300 Subject: [PATCH 6/6] ci: use release bot for CLI publishing --- .github/workflows/backfill-release-assets.yml | 18 +++++++++ .github/workflows/ci.yml | 38 ++++++++++++++++--- CONTRIBUTING.md | 8 ++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backfill-release-assets.yml b/.github/workflows/backfill-release-assets.yml index aa232bd..c1c8088 100644 --- a/.github/workflows/backfill-release-assets.yml +++ b/.github/workflows/backfill-release-assets.yml @@ -32,6 +32,7 @@ jobs: - validate-release-tag runs-on: ${{ matrix.os }} timeout-minutes: 30 + environment: release permissions: contents: write strategy: @@ -46,6 +47,13 @@ jobs: asset_arch: arm64 steps: + - name: Create release bot token + id: release-bot + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3 + with: + app-id: ${{ vars.PUTIO_RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.PUTIO_RELEASE_BOT_PRIVATE_KEY }} + permission-contents: write - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: @@ -96,6 +104,7 @@ jobs: - name: Upload binary assets to the GitHub release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: + token: ${{ steps.release-bot.outputs.token }} tag_name: ${{ inputs.tag_name }} files: | .artifacts/release/* @@ -106,10 +115,18 @@ jobs: - validate-release-tag runs-on: windows-latest timeout-minutes: 30 + environment: release permissions: contents: write steps: + - name: Create release bot token + id: release-bot + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3 + with: + app-id: ${{ vars.PUTIO_RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.PUTIO_RELEASE_BOT_PRIVATE_KEY }} + permission-contents: write - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: @@ -155,6 +172,7 @@ jobs: - name: Upload binary assets to the GitHub release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: + token: ${{ steps.release-bot.outputs.token }} tag_name: ${{ inputs.tag_name }} files: | .artifacts/release/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d23ed9b..d5be762 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,10 +60,20 @@ jobs: new_release_version: ${{ steps.semantic.outputs.new_release_version }} steps: + - name: Create release bot token + id: release-bot + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3 + with: + app-id: ${{ vars.PUTIO_RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.PUTIO_RELEASE_BOT_PRIVATE_KEY }} + permission-contents: write + permission-issues: write + permission-pull-requests: write - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 + token: ${{ steps.release-bot.outputs.token }} - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 @@ -91,12 +101,12 @@ jobs: @semantic-release/git conventional-changelog-conventionalcommits env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.release-bot.outputs.token }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - GIT_AUTHOR_NAME: devsputio - GIT_AUTHOR_EMAIL: devs@put.io - GIT_COMMITTER_NAME: devsputio - GIT_COMMITTER_EMAIL: devs@put.io + GIT_AUTHOR_NAME: ${{ steps.release-bot.outputs.app-slug }}[bot] + GIT_AUTHOR_EMAIL: ${{ steps.release-bot.outputs.app-slug }}[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: ${{ steps.release-bot.outputs.app-slug }}[bot] + GIT_COMMITTER_EMAIL: ${{ steps.release-bot.outputs.app-slug }}[bot]@users.noreply.github.com build-unix-binaries: if: needs.release.outputs.new_release_published == 'true' @@ -105,6 +115,7 @@ jobs: - release runs-on: ${{ matrix.os }} timeout-minutes: 30 + environment: release permissions: contents: write strategy: @@ -119,6 +130,13 @@ jobs: asset_arch: arm64 steps: + - name: Create release bot token + id: release-bot + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3 + with: + app-id: ${{ vars.PUTIO_RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.PUTIO_RELEASE_BOT_PRIVATE_KEY }} + permission-contents: write - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: @@ -167,6 +185,7 @@ jobs: - name: Upload binary assets to the GitHub release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: + token: ${{ steps.release-bot.outputs.token }} tag_name: ${{ needs.release.outputs.new_release_git_tag }} files: | .artifacts/release/* @@ -178,10 +197,18 @@ jobs: - release runs-on: windows-latest timeout-minutes: 30 + environment: release permissions: contents: write steps: + - name: Create release bot token + id: release-bot + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3 + with: + app-id: ${{ vars.PUTIO_RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.PUTIO_RELEASE_BOT_PRIVATE_KEY }} + permission-contents: write - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: @@ -225,6 +252,7 @@ jobs: - name: Upload binary assets to the GitHub release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: + token: ${{ steps.release-bot.outputs.token }} tag_name: ${{ needs.release.outputs.new_release_git_tag }} files: | .artifacts/release/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc55467..780d5f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,14 @@ pnpm run build:sea pnpm run verify:sea ``` +## Release Publishing + +GitHub Actions publishes from `main` through the protected `release` Environment. + +Keep `NPM_TOKEN`, `PUTIO_RELEASE_BOT_APP_ID`, and `PUTIO_RELEASE_BOT_PRIVATE_KEY` in that Environment with required reviewers and prevent self-review enabled. Pull request checks stay secretless and only run verify jobs. + +Release GitHub writes use `putio-release-bot` for version sync commits, `v*` tags, GitHub Releases, and binary asset uploads. Trusted put.io team members may push directly to `main`, but repository rules should block outsiders, force-pushes, and branch deletes where GitHub plan support allows. + ## Development Notes - `verify` is the repository delivery gate.