From 7138ed33e07753adaadec3fafa9c55556968a60f Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 22 May 2026 18:43:23 +0200 Subject: [PATCH 01/33] Replace SITE_DEPLOY_PAT with gh-cli-site-deployer App The release workflow currently checks out github/cli.github.com using a personal access token (SITE_DEPLOY_PAT) owned by an individual maintainer to push generated docs, the index.html version stamp, and RPM/DEB package metadata. Replace that with a short-lived installation token minted at runtime from the new gh-cli-site-deployer GitHub App, owned by the github org and installed only on github/cli.github.com with Contents:write permission. The App's credentials live in the production environment as SITE_DEPLOY_APP_CLIENT_ID and SITE_DEPLOY_APP_PRIVATE_KEY, federated from the github-cli Vault. The token mint step is guarded on inputs.environment == 'production' because non-production environments don't have the App credentials and don't push to the site anyway (the Publish site step is already gated on DO_PUBLISH which requires production). github/cli.github.com is public, so the checkout in non-production falls back to GITHUB_TOKEN for anonymous read access, matching existing behavior. Also update docs/release-process-deep-dive.md to reflect the new flow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 14 +++++++++++++- docs/release-process-deep-dive.md | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index b19a523b60a..d2fe8ae2246 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -288,13 +288,25 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Merge built artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - name: Generate site deploy token + id: site-deploy-token + if: inputs.environment == 'production' + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.SITE_DEPLOY_APP_CLIENT_ID }} + private-key: ${{ secrets.SITE_DEPLOY_APP_PRIVATE_KEY }} + owner: github + repositories: cli.github.com - name: Checkout documentation site uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: github/cli.github.com path: site fetch-depth: 0 - token: ${{ secrets.SITE_DEPLOY_PAT }} + # In non-production environments the App token step is skipped, so + # fall back to github.token for anonymous read access to the public + # cli.github.com repo (push is gated separately on DO_PUBLISH). + token: ${{ steps.site-deploy-token.outputs.token || github.token }} - name: Update site man pages env: GIT_COMMITTER_NAME: cli automation diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index 4e4928be776..09d5b49e6a1 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -442,13 +442,25 @@ release: uses: actions/checkout@v4 - name: Merge built artifacts uses: actions/download-artifact@v4 + - name: Generate site deploy token + id: site-deploy-token + if: inputs.environment == 'production' + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.SITE_DEPLOY_APP_CLIENT_ID }} + private-key: ${{ secrets.SITE_DEPLOY_APP_PRIVATE_KEY }} + owner: github + repositories: cli.github.com - name: Checkout documentation site uses: actions/checkout@v4 with: repository: github/cli.github.com path: site fetch-depth: 0 - token: ${{ secrets.SITE_DEPLOY_PAT }} + # In non-production environments the App token step is skipped, so + # fall back to github.token for anonymous read access to the public + # cli.github.com repo (push is gated separately on DO_PUBLISH). + token: ${{ steps.site-deploy-token.outputs.token || github.token }} - name: Update site man pages env: GIT_COMMITTER_NAME: cli automation From 7d92f7dd5ff6a3749081f8f08eb97a20e1e20bd5 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 16 Jun 2026 11:15:14 +0200 Subject: [PATCH 02/33] Initial conversion to reusable workflow --- .github/workflows/deployment.yml | 37 ++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 79a0ab5cd6d..888359dc6b3 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -11,14 +11,23 @@ permissions: id-token: write on: - workflow_dispatch: + workflow_call: inputs: + repository: + default: cli/cli + required: false + type: string + description: "The repository to release from, in the form 'owner/repo'." + ref: + required: true + type: string + description: "The ref to checkout for the release." tag_name: required: true type: string environment: default: production - type: environment + type: string platforms: default: "linux,macos,windows" type: string @@ -26,6 +35,10 @@ on: description: "Whether to create a GitHub Release" type: boolean default: true + secrets: + CLI_DEPLOY_TOKEN: + required: true + description: "An access token with permissions to read and write to the repository." jobs: validate-tag-name: @@ -47,6 +60,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository || github.repository }} + token: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -90,6 +107,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository || github.repository }} + token: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -170,6 +191,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository || github.repository }} + token: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -284,8 +309,12 @@ jobs: environment: ${{ inputs.environment }} if: inputs.release steps: - - name: Checkout cli/cli + - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository || github.repository }} + token: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} - name: Merge built artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: Generate site deploy token @@ -390,7 +419,7 @@ jobs: # In non-production environments, the assets will not have been signed DO_PUBLISH: ${{ inputs.environment == 'production' }} TAG_NAME: ${{ inputs.tag_name }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} run: | shopt -s failglob pushd dist From 8f2ecd2ee83fa2e3f980be37bb1c13e96d44b096 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 16 Jun 2026 14:42:00 +0200 Subject: [PATCH 03/33] Update deployment.yml --- .github/workflows/deployment.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 888359dc6b3..b01640ac4c0 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -36,9 +36,12 @@ on: type: boolean default: true secrets: - CLI_DEPLOY_TOKEN: + DEPLOY_APP_ID: required: true - description: "An access token with permissions to read and write to the repository." + description: "The GitHub App ID used to generate deploy tokens." + DEPLOY_APP_PRIVATE_KEY: + required: true + description: "The private key for the GitHub App used to generate deploy tokens." jobs: validate-tag-name: @@ -58,12 +61,20 @@ jobs: environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'linux') steps: + - name: Generate deploy token + id: deploy-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.DEPLOY_APP_ID }} + private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} + owner: cli + repositories: cli - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} repository: ${{ inputs.repository || github.repository }} - token: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} + token: ${{ steps.deploy-token.outputs.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: From 9522f6ebac9e2ef2686e850f5589e95c0e071667 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 17 Jun 2026 06:54:25 +0200 Subject: [PATCH 04/33] Mint a token from GitHub app --- .github/workflows/deployment.yml | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index b01640ac4c0..f55a6aa0063 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -116,12 +116,20 @@ jobs: environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'macos') steps: + - name: Generate deploy token + id: deploy-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.DEPLOY_APP_ID }} + private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} + owner: cli + repositories: cli - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} repository: ${{ inputs.repository || github.repository }} - token: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} + token: ${{ steps.deploy-token.outputs.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -200,12 +208,20 @@ jobs: environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'windows') steps: + - name: Generate deploy token + id: deploy-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.DEPLOY_APP_ID }} + private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} + owner: cli + repositories: cli - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} repository: ${{ inputs.repository || github.repository }} - token: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} + token: ${{ steps.deploy-token.outputs.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -320,12 +336,20 @@ jobs: environment: ${{ inputs.environment }} if: inputs.release steps: + - name: Generate deploy token + id: deploy-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.DEPLOY_APP_ID }} + private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} + owner: cli + repositories: cli - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} repository: ${{ inputs.repository || github.repository }} - token: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} + token: ${{ steps.deploy-token.outputs.token }} - name: Merge built artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: Generate site deploy token @@ -430,7 +454,7 @@ jobs: # In non-production environments, the assets will not have been signed DO_PUBLISH: ${{ inputs.environment == 'production' }} TAG_NAME: ${{ inputs.tag_name }} - GH_TOKEN: ${{ secrets.CLI_DEPLOY_TOKEN || github.token }} + GH_TOKEN: ${{ steps.deploy-token.outputs.token }} run: | shopt -s failglob pushd dist From 9ac613ac339727400ced8cbe5ccc7f1e8f0ee0c5 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 17 Jun 2026 11:22:46 +0200 Subject: [PATCH 05/33] Use client-id input for create-github-app-token Fix workflow inputs for actions/create-github-app-token by replacing the incorrect app-id key with client-id in multiple deploy job steps. This corrects the parameter name (four occurrences) so the Action receives the expected secret (DEPLOY_APP_ID) and can generate deployment tokens successfully. No other changes to the workflow or action version were made. --- .github/workflows/deployment.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index f55a6aa0063..ae1ce5eddd1 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -65,7 +65,7 @@ jobs: id: deploy-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.DEPLOY_APP_ID }} + client-id: ${{ secrets.DEPLOY_APP_ID }} private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} owner: cli repositories: cli @@ -120,7 +120,7 @@ jobs: id: deploy-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.DEPLOY_APP_ID }} + client-id: ${{ secrets.DEPLOY_APP_ID }} private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} owner: cli repositories: cli @@ -212,7 +212,7 @@ jobs: id: deploy-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.DEPLOY_APP_ID }} + client-id: ${{ secrets.DEPLOY_APP_ID }} private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} owner: cli repositories: cli @@ -340,7 +340,7 @@ jobs: id: deploy-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.DEPLOY_APP_ID }} + client-id: ${{ secrets.DEPLOY_APP_ID }} private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} owner: cli repositories: cli From f8dec084bc9ccc9e6498288fb3433b268fcaad71 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 17 Jun 2026 11:30:37 +0200 Subject: [PATCH 06/33] Rename DEPLOY_APP_ID secret to DEPLOY_APP_CLIENT_ID Update .github/workflows/deployment.yml to rename the deploy secret from DEPLOY_APP_ID to DEPLOY_APP_CLIENT_ID and adjust all create-github-app-token steps to use the new secret (client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }}). This clarifies that the secret is the GitHub App Client ID used to generate deploy tokens and keeps the workflow consistent across multiple deploy-token job invocations. --- .github/workflows/deployment.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index ae1ce5eddd1..aa92ac61949 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -36,9 +36,9 @@ on: type: boolean default: true secrets: - DEPLOY_APP_ID: + DEPLOY_APP_CLIENT_ID: required: true - description: "The GitHub App ID used to generate deploy tokens." + description: "The GitHub App Client ID used to generate deploy tokens." DEPLOY_APP_PRIVATE_KEY: required: true description: "The private key for the GitHub App used to generate deploy tokens." @@ -65,7 +65,7 @@ jobs: id: deploy-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - client-id: ${{ secrets.DEPLOY_APP_ID }} + client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }} private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} owner: cli repositories: cli @@ -120,7 +120,7 @@ jobs: id: deploy-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - client-id: ${{ secrets.DEPLOY_APP_ID }} + client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }} private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} owner: cli repositories: cli @@ -212,7 +212,7 @@ jobs: id: deploy-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - client-id: ${{ secrets.DEPLOY_APP_ID }} + client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }} private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} owner: cli repositories: cli @@ -340,7 +340,7 @@ jobs: id: deploy-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - client-id: ${{ secrets.DEPLOY_APP_ID }} + client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }} private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} owner: cli repositories: cli From 4f430685bf685ad9bacec24af3f7ffec214ee04a Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 17 Jun 2026 13:07:36 +0200 Subject: [PATCH 07/33] Update deployment.yml --- .github/workflows/deployment.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index aa92ac61949..f3bea164245 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -37,11 +37,7 @@ on: default: true secrets: DEPLOY_APP_CLIENT_ID: - required: true - description: "The GitHub App Client ID used to generate deploy tokens." DEPLOY_APP_PRIVATE_KEY: - required: true - description: "The private key for the GitHub App used to generate deploy tokens." jobs: validate-tag-name: From f69cf0f010829ae95a4e83387d9ef1ea1b680163 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 30 Jun 2026 13:20:09 +0200 Subject: [PATCH 08/33] Add 20-minute timeouts to deploy jobs Set `timeout-minutes: 20` for the Linux, macOS, and Windows deployment jobs in the deployment workflow to prevent stalled runs from hanging indefinitely. --- .github/workflows/deployment.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index f3bea164245..d2e4629ea42 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -56,6 +56,7 @@ jobs: runs-on: ubuntu-latest environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'linux') + timeout-minutes: 20 steps: - name: Generate deploy token id: deploy-token @@ -111,6 +112,7 @@ jobs: runs-on: macos-latest environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'macos') + timeout-minutes: 20 steps: - name: Generate deploy token id: deploy-token @@ -203,6 +205,7 @@ jobs: runs-on: windows-2022 environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'windows') + timeout-minutes: 20 steps: - name: Generate deploy token id: deploy-token From 83f9562f604140cce2af19cc6340b1e0d249f68b Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 30 Jun 2026 13:33:31 +0200 Subject: [PATCH 09/33] Skip Windows code signing outside production deploys Gate signing in sign.ps1 on a new DO_PUBLISH env var, set in the deployment workflow to true only for production. This avoids attempting code signing in non-production environments, consistent with the existing DO_PUBLISH gating used elsewhere in the workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 1 + script/sign.ps1 | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index d2e4629ea42..4390613c3a3 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -276,6 +276,7 @@ jobs: DLIB_PATH: ${{ runner.temp }}\acs\bin\x64\Azure.CodeSigning.Dlib.dll METADATA_PATH: ${{ runner.temp }}\acs\metadata.json TAG_NAME: ${{ inputs.tag_name }} + DO_PUBLISH: ${{ inputs.environment == 'production' }} run: script/release --local "$TAG_NAME" --platform windows - name: Set up MSBuild id: setupmsbuild diff --git a/script/sign.ps1 b/script/sign.ps1 index f64ec1e72e9..d64df6e5c6b 100644 --- a/script/sign.ps1 +++ b/script/sign.ps1 @@ -1,5 +1,11 @@ #!/usr/bin/env pwsh +# If DO_PUBLISH is not set or is set to "false", skip signing +if ($null -eq $Env:DO_PUBLISH -or $Env:DO_PUBLISH -eq "false") { + Write-Host "Skipping Windows code signing; DO_PUBLISH not set or false" + exit +} + if ($null -eq $Env:DLIB_PATH) { Write-Host "Skipping Windows code signing; DLIB_PATH not set" exit From e64de0ce5da5affdda65ab1bf25e75aba2e273bd Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 1 Jul 2026 12:31:49 +0200 Subject: [PATCH 10/33] Add dry_run flag to gate release publishing Introduce a dry_run input (default true) to the deployment workflow so artifacts are built and signed but no GitHub Release is created and no site is published. Rename DO_PUBLISH to DO_SIGN_ARTIFACTS for the Windows signing path so signing remains driven solely by environment, independent of publish gating, and set it on the MSI signing step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 13 +++++++++---- script/sign.ps1 | 5 ++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 4390613c3a3..f0e69679639 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -35,6 +35,10 @@ on: description: "Whether to create a GitHub Release" type: boolean default: true + dry_run: + description: "Whether to sign but not publish a release." + type: boolean + default: true secrets: DEPLOY_APP_CLIENT_ID: DEPLOY_APP_PRIVATE_KEY: @@ -276,7 +280,7 @@ jobs: DLIB_PATH: ${{ runner.temp }}\acs\bin\x64\Azure.CodeSigning.Dlib.dll METADATA_PATH: ${{ runner.temp }}\acs\metadata.json TAG_NAME: ${{ inputs.tag_name }} - DO_PUBLISH: ${{ inputs.environment == 'production' }} + DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }} run: script/release --local "$TAG_NAME" --platform windows - name: Set up MSBuild id: setupmsbuild @@ -317,6 +321,7 @@ jobs: AZURE_TENANT_ID: ${{ secrets.SPN_GITHUB_CLI_SIGNING_TENANT_ID }} DLIB_PATH: ${{ runner.temp }}\acs\bin\x64\Azure.CodeSigning.Dlib.dll METADATA_PATH: ${{ runner.temp }}\acs\metadata.json + DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }} # Technically this could just be true since we don't run this step if the environment is not production, but we keep it the same as the build step for consistency. run: | Get-ChildItem -Path .\dist -Filter *.msi | ForEach-Object { .\script\sign.ps1 $_.FullName @@ -452,7 +457,7 @@ jobs: - name: Create the release env: # In non-production environments, the assets will not have been signed - DO_PUBLISH: ${{ inputs.environment == 'production' }} + DO_PUBLISH: ${{ inputs.environment == 'production' && !inputs.dry_run }} TAG_NAME: ${{ inputs.tag_name }} GH_TOKEN: ${{ steps.deploy-token.outputs.token }} run: | @@ -475,7 +480,7 @@ jobs: script/label-assets dist/gh_* | xargs $guard gh release create "${release_args[@]}" -- - name: Publish site env: - DO_PUBLISH: ${{ inputs.environment == 'production' && !contains(inputs.tag_name, '-') }} + DO_PUBLISH: ${{ inputs.environment == 'production' && !contains(inputs.tag_name, '-') && !inputs.dry_run }} TAG_NAME: ${{ inputs.tag_name }} GIT_COMMITTER_NAME: cli automation GIT_AUTHOR_NAME: cli automation @@ -490,4 +495,4 @@ jobs: else git log --oneline @{upstream}.. git diff --name-status @{upstream}.. - fi \ No newline at end of file + fi diff --git a/script/sign.ps1 b/script/sign.ps1 index d64df6e5c6b..3cf3a355b44 100644 --- a/script/sign.ps1 +++ b/script/sign.ps1 @@ -1,8 +1,7 @@ #!/usr/bin/env pwsh -# If DO_PUBLISH is not set or is set to "false", skip signing -if ($null -eq $Env:DO_PUBLISH -or $Env:DO_PUBLISH -eq "false") { - Write-Host "Skipping Windows code signing; DO_PUBLISH not set or false" +if ($null -eq $Env:DO_SIGN_ARTIFACTS -or $Env:DO_SIGN_ARTIFACTS -eq "false") { + Write-Host "Skipping Windows code signing; DO_SIGN_ARTIFACTS not set or false" exit } From 0e3afb497d7e675b674bf8bce4f9fc4bfd917c0e Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 1 Jul 2026 14:46:16 +0200 Subject: [PATCH 11/33] Fix macOS signing/notarization keychain and gating Rework the macOS deployment signing to use App Store Connect API key based notarization and a Developer ID certificate identifier. - Use a single consistent keychain (build.keychain) for signing and notarization; the notarization credentials previously targeted a keychain that was never created. - Gate the App Store Connect API key and notarization credential steps on the production environment so non-production builds (with empty secrets) no longer fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 54 ++++++++++++++++++++++++++------ script/sign | 13 +++++--- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index f0e69679639..6ce33abad8d 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -42,7 +42,22 @@ on: secrets: DEPLOY_APP_CLIENT_ID: DEPLOY_APP_PRIVATE_KEY: - + GATEWATCHER_APP_STORE_CONNECT_API_BASE64_PRIVATE_KEY: + GATEWATCHER_APP_STORE_CONNECT_API_ISSUER_ID: + GATEWATCHER_APP_STORE_CONNECT_API_KEY_ID: + GATEWATCHER_DEVELOPER_ID_CERT: + GATEWATCHER_DEVELOPER_ID_CERT_IDENTIFIER: + GATEWATCHER_DEVELOPER_ID_PASSWORD: + GPG_KEY: + GPG_KEYGRIP: + GPG_PASSPHRASE: + GPG_PUBKEY: + SITE_DEPLOY_APP_CLIENT_ID: + SITE_DEPLOY_APP_PRIVATE_KEY: + SPN_GITHUB_CLI_SIGNING: + SPN_GITHUB_CLI_SIGNING_CLIENT_ID: + SPN_GITHUB_CLI_SIGNING_TENANT_ID: + jobs: validate-tag-name: runs-on: ubuntu-latest @@ -136,14 +151,14 @@ jobs: uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: 'go.mod' + - name: Configure macOS signing if: inputs.environment == 'production' env: - APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} - APPLE_APPLICATION_CERT: ${{ secrets.APPLE_APPLICATION_CERT }} - APPLE_APPLICATION_CERT_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERT_PASSWORD }} + APPLE_APPLICATION_CERT: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }} + APPLE_APPLICATION_CERT_PASSWORD: ${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }} run: | - keychain="$RUNNER_TEMP/buildagent.keychain" + keychain="$RUNNER_TEMP/build.keychain" keychain_password="password1" security create-keychain -p "$keychain_password" "$keychain" @@ -154,6 +169,27 @@ jobs: security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$APPLE_APPLICATION_CERT_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain" rm "$RUNNER_TEMP/cert.p12" + + - name: Add App Store Connect API key to keychain + if: inputs.environment == 'production' + uses: nodeselector/setup-apple-codesign@ab275d0f6fb63ef9e20b12b42ea0d567f935723c + id: setup-apple-codesign + with: + asset-type: "app-store-connect-api-key" + app-store-connect-api-key-key-id: ${{ secrets.GATEWATCHER_APP_STORE_CONNECT_API_KEY_ID }} + app-store-connect-api-key-issuer-id: ${{ secrets.GATEWATCHER_APP_STORE_CONNECT_API_ISSUER_ID }} + app-store-connect-api-key-base64-private-key: ${{ secrets.GATEWATCHER_APP_STORE_CONNECT_API_BASE64_PRIVATE_KEY }} + + - name: Configure notarization credentials + if: inputs.environment == 'production' + shell: bash + run: | + xcrun notarytool store-credentials "notarytool-password" \ + --key "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-key-path }}" \ + --key-id "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-key-id }}" \ + --issuer "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-issuer-id }}" \ + --keychain $RUNNER_TEMP/build.keychain + - name: Install GoReleaser uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: @@ -170,14 +206,14 @@ jobs: - name: Build release binaries env: TAG_NAME: ${{ inputs.tag_name }} - APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} + KEYCHAIN: ${{ runner.temp }}/build.keychain + DEVELOPER_ID_CERT_IDENTIFIER: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT_IDENTIFIER }} run: script/release --local "$TAG_NAME" --platform macos - name: Notarize macOS archives if: inputs.environment == 'production' env: - APPLE_ID: ${{ vars.APPLE_ID }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} + DEVELOPER_ID_CERT_IDENTIFIER: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT_IDENTIFIER }} + KEYCHAIN: ${{ runner.temp }}/build.keychain run: | shopt -s failglob script/sign dist/gh_*_macOS_*.zip diff --git a/script/sign b/script/sign index f07a7d2d46e..569fbc770c6 100755 --- a/script/sign +++ b/script/sign @@ -6,15 +6,20 @@ set -e sign_macos() { - if [[ -z "$APPLE_DEVELOPER_ID" ]]; then - echo "skipping macOS code-signing; APPLE_DEVELOPER_ID not set" >&2 + if [[ -z "$DEVELOPER_ID_CERT_IDENTIFIER" ]]; then + echo "skipping macOS code-signing; DEVELOPER_ID_CERT_IDENTIFIER not set" >&2 + return 0 + fi + + if [[ -z "$KEYCHAIN" ]]; then + echo "skipping macOS code-signing; KEYCHAIN not set" >&2 return 0 fi if [[ $1 == *.zip ]]; then - xcrun notarytool submit "$1" --apple-id "${APPLE_ID?}" --team-id "${APPLE_DEVELOPER_ID?}" --password "${APPLE_ID_PASSWORD?}" + xcrun notarytool submit "$1" --keychain $KEYCHAIN --keychain-profile "notarytool-password" --wait else - codesign --timestamp --options=runtime -s "${APPLE_DEVELOPER_ID?}" -v "$1" + codesign --timestamp --options=runtime -s "${DEVELOPER_ID_CERT_IDENTIFIER?}" -v "$1" fi } From 57e8cce5f1688a1f6ce470fdcede0d568136cff6 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 1 Jul 2026 15:09:15 +0200 Subject: [PATCH 12/33] Update deployment.yml --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 6ce33abad8d..5f3a7b09703 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -456,7 +456,7 @@ jobs: cp script/rpmmacros ~/.rpmmacros rpmsign --addsign dist/*.rpm - name: Attest release artifacts - if: inputs.environment == 'production' + if: inputs.environment == 'production' && !inputs.dry_run uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: "dist/gh_*" From 9e376410f3bca7927e36a9a2a79962a3ab576873 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 1 Jul 2026 15:46:01 +0200 Subject: [PATCH 13/33] Use workflow_dispatch for deployment flow Convert the deployment workflow from reusable `workflow_call` to manual `workflow_dispatch`, removing the extra `repository` input and forwarded secret declarations. Drop per-job deploy app token generation and custom checkout token/repository overrides, relying on the default checkout context instead. Update release publishing to use `secrets.GITHUB_TOKEN` for GH CLI authentication. --- .github/workflows/deployment.yml | 67 +------------------------------- 1 file changed, 2 insertions(+), 65 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 5f3a7b09703..cf1ff06b86d 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -11,13 +11,8 @@ permissions: id-token: write on: - workflow_call: + workflow_dispatch: inputs: - repository: - default: cli/cli - required: false - type: string - description: "The repository to release from, in the form 'owner/repo'." ref: required: true type: string @@ -39,24 +34,6 @@ on: description: "Whether to sign but not publish a release." type: boolean default: true - secrets: - DEPLOY_APP_CLIENT_ID: - DEPLOY_APP_PRIVATE_KEY: - GATEWATCHER_APP_STORE_CONNECT_API_BASE64_PRIVATE_KEY: - GATEWATCHER_APP_STORE_CONNECT_API_ISSUER_ID: - GATEWATCHER_APP_STORE_CONNECT_API_KEY_ID: - GATEWATCHER_DEVELOPER_ID_CERT: - GATEWATCHER_DEVELOPER_ID_CERT_IDENTIFIER: - GATEWATCHER_DEVELOPER_ID_PASSWORD: - GPG_KEY: - GPG_KEYGRIP: - GPG_PASSPHRASE: - GPG_PUBKEY: - SITE_DEPLOY_APP_CLIENT_ID: - SITE_DEPLOY_APP_PRIVATE_KEY: - SPN_GITHUB_CLI_SIGNING: - SPN_GITHUB_CLI_SIGNING_CLIENT_ID: - SPN_GITHUB_CLI_SIGNING_TENANT_ID: jobs: validate-tag-name: @@ -77,20 +54,10 @@ jobs: if: contains(inputs.platforms, 'linux') timeout-minutes: 20 steps: - - name: Generate deploy token - id: deploy-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }} - private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} - owner: cli - repositories: cli - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} - repository: ${{ inputs.repository || github.repository }} - token: ${{ steps.deploy-token.outputs.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -133,20 +100,10 @@ jobs: if: contains(inputs.platforms, 'macos') timeout-minutes: 20 steps: - - name: Generate deploy token - id: deploy-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }} - private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} - owner: cli - repositories: cli - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} - repository: ${{ inputs.repository || github.repository }} - token: ${{ steps.deploy-token.outputs.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -247,20 +204,10 @@ jobs: if: contains(inputs.platforms, 'windows') timeout-minutes: 20 steps: - - name: Generate deploy token - id: deploy-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }} - private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} - owner: cli - repositories: cli - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} - repository: ${{ inputs.repository || github.repository }} - token: ${{ steps.deploy-token.outputs.token }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -377,20 +324,10 @@ jobs: environment: ${{ inputs.environment }} if: inputs.release steps: - - name: Generate deploy token - id: deploy-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.DEPLOY_APP_CLIENT_ID }} - private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} - owner: cli - repositories: cli - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} - repository: ${{ inputs.repository || github.repository }} - token: ${{ steps.deploy-token.outputs.token }} - name: Merge built artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: Generate site deploy token @@ -495,7 +432,7 @@ jobs: # In non-production environments, the assets will not have been signed DO_PUBLISH: ${{ inputs.environment == 'production' && !inputs.dry_run }} TAG_NAME: ${{ inputs.tag_name }} - GH_TOKEN: ${{ steps.deploy-token.outputs.token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | shopt -s failglob pushd dist From d6be637c502c5d19732a10899045ac36490a7bf5 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 1 Jul 2026 16:04:35 +0200 Subject: [PATCH 14/33] Update deployment.yml --- .github/workflows/deployment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index cf1ff06b86d..40d6357bf05 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -17,6 +17,7 @@ on: required: true type: string description: "The ref to checkout for the release." + default: "trunk" tag_name: required: true type: string From 3b24ab7340d6bcd950723e9cc45239885a41886e Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 1 Jul 2026 16:07:04 +0200 Subject: [PATCH 15/33] Update deployment.yml --- .github/workflows/deployment.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 40d6357bf05..fe6650a4ec9 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -21,18 +21,21 @@ on: tag_name: required: true type: string + description: "The tag name for the release (e.g. v2.100.0)." environment: default: production - type: string + type: environment + description: "The deployment environment..." platforms: default: "linux,macos,windows" type: string + description: "Comma-separated list of platforms to build." release: description: "Whether to create a GitHub Release" type: boolean default: true dry_run: - description: "Whether to sign but not publish a release." + description: "Perform a dry run without publishing artifacts or creating a release" type: boolean default: true From 637455cb20a501d3cfe6fd7b92e24fad561acc27 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 12:33:55 +0200 Subject: [PATCH 16/33] Let's try a different approach --- .github/workflows/deployment.yml | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index fe6650a4ec9..9a9a46050d3 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -113,23 +113,21 @@ jobs: with: go-version-file: 'go.mod' - - name: Configure macOS signing + - name: Install code signing certificate if: inputs.environment == 'production' - env: - APPLE_APPLICATION_CERT: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }} - APPLE_APPLICATION_CERT_PASSWORD: ${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }} + shell: bash run: | - keychain="$RUNNER_TEMP/build.keychain" - keychain_password="password1" - - security create-keychain -p "$keychain_password" "$keychain" - security default-keychain -s "$keychain" - security unlock-keychain -p "$keychain_password" "$keychain" + # create a keychain for the certificate + PW=pwd.${{ github.run_number }} + security create-keychain -p $PW $RUNNER_TEMP/build.keychain + security set-keychain-settings -lut 21600 "$RUNNER_TEMP/build.keychain" + security default-keychain -s $RUNNER_TEMP/build.keychain + security unlock-keychain -p $PW $RUNNER_TEMP/build.keychain - base64 -D <<<"$APPLE_APPLICATION_CERT" > "$RUNNER_TEMP/cert.p12" - security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$APPLE_APPLICATION_CERT_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain" - rm "$RUNNER_TEMP/cert.p12" + # import the certificate + echo '${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }}' | base64 -d > $RUNNER_TEMP/cert.p12 + security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}' -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain - name: Add App Store Connect API key to keychain if: inputs.environment == 'production' From 86ded778a0fcb60f4770a5626dce8f8d41b06bec Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 12:36:47 +0200 Subject: [PATCH 17/33] Add diagnostic step to list macOS signing identities Helps debug 'no identity found' failures by printing the codesigning identities available in the build keychain, so DEVELOPER_ID_CERT_IDENTIFIER can be verified against the actual imported certificate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 9a9a46050d3..b4eedfd7220 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -129,6 +129,15 @@ jobs: security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}' -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain + - name: List available signing identities + if: inputs.environment == 'production' + shell: bash + run: | + # Prints each codesigning identity's SHA-1 and quoted common name. + # Compare this against DEVELOPER_ID_CERT_IDENTIFIER: codesign fails + # with "no identity found" when the two don't match. + security find-identity -v -p codesigning "$RUNNER_TEMP/build.keychain" + - name: Add App Store Connect API key to keychain if: inputs.environment == 'production' uses: nodeselector/setup-apple-codesign@ab275d0f6fb63ef9e20b12b42ea0d567f935723c From 0d8d855984d8ea4c10584d16ae49537f8e46558c Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 12:53:03 +0200 Subject: [PATCH 18/33] Try this then --- .github/workflows/deployment.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index b4eedfd7220..0b846e03980 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -175,12 +175,12 @@ jobs: env: TAG_NAME: ${{ inputs.tag_name }} KEYCHAIN: ${{ runner.temp }}/build.keychain - DEVELOPER_ID_CERT_IDENTIFIER: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT_IDENTIFIER }} + DEVELOPER_ID_CERT_IDENTIFIER: ${{ vars.MAC_APP_SIGNING_IDENTITY }} run: script/release --local "$TAG_NAME" --platform macos - name: Notarize macOS archives if: inputs.environment == 'production' env: - DEVELOPER_ID_CERT_IDENTIFIER: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT_IDENTIFIER }} + DEVELOPER_ID_CERT_IDENTIFIER: ${{ vars.MAC_APP_SIGNING_IDENTITY }} KEYCHAIN: ${{ runner.temp }}/build.keychain run: | shopt -s failglob From 52669ebd7df97a88c693cbeb13761ba894a950e1 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 13:05:16 +0200 Subject: [PATCH 19/33] Don't see how this could have worked in the past --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 0b846e03980..03ddbe35ab5 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -194,7 +194,7 @@ jobs: if: inputs.environment == 'production' env: TAG_NAME: ${{ inputs.tag_name }} - APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.APPLE_DEVELOPER_INSTALLER_ID }} + APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.MAC_APP_SIGNING_IDENTITY }} run: | shopt -s failglob script/pkgmacos "$TAG_NAME" From ef9066483b11d26d1a733547c4839c2153f3ae8d Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 14:06:24 +0200 Subject: [PATCH 20/33] Don't need this any more --- .github/workflows/deployment.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 03ddbe35ab5..d0bd8687ecb 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -129,15 +129,6 @@ jobs: security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}' -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain - - name: List available signing identities - if: inputs.environment == 'production' - shell: bash - run: | - # Prints each codesigning identity's SHA-1 and quoted common name. - # Compare this against DEVELOPER_ID_CERT_IDENTIFIER: codesign fails - # with "no identity found" when the two don't match. - security find-identity -v -p codesigning "$RUNNER_TEMP/build.keychain" - - name: Add App Store Connect API key to keychain if: inputs.environment == 'production' uses: nodeselector/setup-apple-codesign@ab275d0f6fb63ef9e20b12b42ea0d567f935723c From eeb9a3c772bf4d1b8cae3a333cf24459464d4566 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 14:55:43 +0200 Subject: [PATCH 21/33] Update release deep-dive doc for macOS signing and dry_run changes Reflects the reworked macOS code-signing/notarization flow (dedicated build.keychain, App Store Connect API key, notarytool keychain profile, DEVELOPER_ID_CERT_IDENTIFIER/MAC_APP_SIGNING_IDENTITY), documents the new dry_run input and its publishing gates, the ref input and per-job timeouts, the Windows DO_SIGN_ARTIFACTS flag, and the GitHub App site deploy token replacing SITE_DEPLOY_PAT. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-process-deep-dive.md | 130 +++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 40 deletions(-) diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index 98636352000..43e2a55f59d 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -26,7 +26,9 @@ This section will deep dive into each job in the [`deployment.yml` workflow](htt Although this workflow is used to do our production releases for Linux, MacOS and Windows, it is also possible to run subsets of the workflow. Specifically: * The workflow can be triggered with `inputs.release` set to `false`, resulting in the entire [release job](#release) being skipped. This is not exposed via `./script/release`. * Many sections are guarded by `if: inputs.environment == 'production'`. These guards protect sections that require secrets (e.g. signing) or that result in mutations (e.g. creating a GitHub release). `./script/release` accepts the `--staging` flag for this purpose. This differs from the previous bullet point as some steps in the [release job](#release) print debug information such as [`git` diffs](https://github.com/cli/cli/blob/5d2eadef8cccf2671f68aad05cd93215a4c01b48/.github/workflows/deployment.yml#L380-L384). + * The workflow can be triggered with `inputs.dry_run` set to `true` (the default for the `workflow_dispatch` form). Unlike `inputs.environment`, a dry run still exercises the `production` signing and packaging steps, but it does **not** publish anything: creating GitHub attestations, creating the GitHub Release, and pushing to the `cli.github.com` site repository are all skipped. This makes it possible to validate a full production build, including signing, without mutating anything externally visible. See [Publishing behaviour and dry runs](#dry-run) for details. * The workflow can be triggered for only `linux`, `MacOS` or `Windows` which allows for debugging single jobs. This is not exposed via `./script/release`. The [release job](#release) should not run in this case as it [requires all OS specific builds.](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L252) + * The workflow accepts an `inputs.ref` input (defaulting to `trunk`) that controls the git ref checked out by every job, allowing a release to be built from a branch or specific commit. ## [validate-tag-name](https://github.com/cli/cli/blob/537a22228cd6b42b740d7f1c09f47c45bb1dab30/.github/workflows/deployment.yml#L31-L39) @@ -52,7 +54,7 @@ The purpose of this job is to [prevent incorrectly tagged releases](https://gith ## [OS Builds](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L40-L248) -After validating the tag name, the workflow parallelises across `ubuntu`, `macos` and `windows` runners. The primary purpose of these jobs is to build and sign release artifacts. These artifacts are made available to the `release` job via `actions/upload-artifact` and `actions/download-artifact` respectively. +After validating the tag name, the workflow parallelises across `ubuntu`, `macos` and `windows` runners. The primary purpose of these jobs is to build and sign release artifacts. These artifacts are made available to the `release` job via `actions/upload-artifact` and `actions/download-artifact` respectively. Each of these jobs (as well as `release`) checks out the ref specified by `inputs.ref` and sets `timeout-minutes: 20` so a hung build (for example, a code-signing step waiting on a remote service) fails fast rather than consuming the full default job timeout. ### [linux](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L40-L73) @@ -123,24 +125,39 @@ There is no signing of linux artifacts in this job. See the [release job](#relea uses: actions/setup-go@v5 with: go-version-file: 'go.mod' - - name: Configure macOS signing + - name: Install code signing certificate if: inputs.environment == 'production' - env: - APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} - APPLE_APPLICATION_CERT: ${{ secrets.APPLE_APPLICATION_CERT }} - APPLE_APPLICATION_CERT_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERT_PASSWORD }} + shell: bash run: | - keychain="$RUNNER_TEMP/buildagent.keychain" - keychain_password="password1" - - security create-keychain -p "$keychain_password" "$keychain" - security default-keychain -s "$keychain" - security unlock-keychain -p "$keychain_password" "$keychain" - - base64 -D <<<"$APPLE_APPLICATION_CERT" > "$RUNNER_TEMP/cert.p12" - security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$APPLE_APPLICATION_CERT_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain" - rm "$RUNNER_TEMP/cert.p12" + # create a keychain for the certificate + PW=pwd.${{ github.run_number }} + security create-keychain -p $PW $RUNNER_TEMP/build.keychain + security set-keychain-settings -lut 21600 "$RUNNER_TEMP/build.keychain" + security default-keychain -s $RUNNER_TEMP/build.keychain + security unlock-keychain -p $PW $RUNNER_TEMP/build.keychain + + # import the certificate + echo '${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }}' | base64 -d > $RUNNER_TEMP/cert.p12 + security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}' -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain + - name: Add App Store Connect API key to keychain + if: inputs.environment == 'production' + uses: nodeselector/setup-apple-codesign@ab275d0f6fb63ef9e20b12b42ea0d567f935723c + id: setup-apple-codesign + with: + asset-type: "app-store-connect-api-key" + app-store-connect-api-key-key-id: ${{ secrets.GATEWATCHER_APP_STORE_CONNECT_API_KEY_ID }} + app-store-connect-api-key-issuer-id: ${{ secrets.GATEWATCHER_APP_STORE_CONNECT_API_ISSUER_ID }} + app-store-connect-api-key-base64-private-key: ${{ secrets.GATEWATCHER_APP_STORE_CONNECT_API_BASE64_PRIVATE_KEY }} + - name: Configure notarization credentials + if: inputs.environment == 'production' + shell: bash + run: | + xcrun notarytool store-credentials "notarytool-password" \ + --key "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-key-path }}" \ + --key-id "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-key-id }}" \ + --issuer "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-issuer-id }}" \ + --keychain $RUNNER_TEMP/build.keychain - name: Install GoReleaser uses: goreleaser/goreleaser-action@v6 with: @@ -149,14 +166,14 @@ There is no signing of linux artifacts in this job. See the [release job](#relea - name: Build release binaries env: TAG_NAME: ${{ inputs.tag_name }} - APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} + KEYCHAIN: ${{ runner.temp }}/build.keychain + DEVELOPER_ID_CERT_IDENTIFIER: ${{ vars.MAC_APP_SIGNING_IDENTITY }} run: script/release --local "$TAG_NAME" --platform macos - name: Notarize macOS archives if: inputs.environment == 'production' env: - APPLE_ID: ${{ vars.APPLE_ID }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} + DEVELOPER_ID_CERT_IDENTIFIER: ${{ vars.MAC_APP_SIGNING_IDENTITY }} + KEYCHAIN: ${{ runner.temp }}/build.keychain run: | shopt -s failglob script/sign dist/gh_*_macOS_*.zip @@ -169,7 +186,7 @@ There is no signing of linux artifacts in this job. See the [release job](#relea if: inputs.environment == 'production' env: TAG_NAME: ${{ inputs.tag_name }} - APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.APPLE_DEVELOPER_INSTALLER_ID }} + APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.MAC_APP_SIGNING_IDENTITY }} run: | shopt -s failglob script/pkgmacos "$TAG_NAME" @@ -203,33 +220,42 @@ There are three levels of "signing" that occur in this job: > [!WARNING] > Although the job title is `Build & notarize universal macOS pkg installer`, the [`productbuild` docs](https://www.unix.com/man_page/osx/1/productbuild/) only refer to signing, thus notarization may not be the correct term here. -> [!WARNING] -> Although it appears as if signing the `.pkg` installer can occur if `inputs.environment == 'production'`, in practice, I don't believe we ever set `${{ vars.APPLE_DEVELOPER_INSTALLER_ID }}`, thus we always [skip signing](https://github.com/cli/cli/actions/runs/13271193192/job/37050749548#step:9:11). +> [!NOTE] +> Historically the `.pkg` installer was never actually signed because `${{ vars.APPLE_DEVELOPER_INSTALLER_ID }}` was [never set](https://github.com/cli/cli/actions/runs/13271193192/job/37050749548#step:9:11). The `Build & notarize universal macOS pkg installer` step now passes `APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.MAC_APP_SIGNING_IDENTITY }}`, so `productbuild` signing runs when that repository variable is populated. Signing of MacOS artifacts uses `codesign` and notarization uses `xcrun notarytool`, which submits the artifact to the Apple servers for additional checks. +Signing and notarization are set up across three steps that run only when `inputs.environment == 'production'`: + +1. **Install code signing certificate** creates a dedicated keychain and imports the Developer ID Application certificate used by `codesign`. +2. **Add App Store Connect API key to keychain** uses the [`nodeselector/setup-apple-codesign`](https://github.com/nodeselector/setup-apple-codesign) action to materialise the App Store Connect API key (`.p8`) that `notarytool` authenticates with, exposing its key path, key id and issuer id as step outputs. +3. **Configure notarization credentials** runs `xcrun notarytool store-credentials` to persist those API key details into the keychain under the profile name `notarytool-password`, so later `notarytool submit` calls can reference the profile instead of passing credentials directly. + In order to perform signing, a keychain must be configured with the signing certificate. Comments have been added to provide clarity to the script: ```sh -keychain="$RUNNER_TEMP/buildagent.keychain" -keychain_password="password1" +# Derive a per-run keychain password so it is not hard-coded. +PW=pwd.${{ github.run_number }} # Create a new keychain for credentials to be stored in. -security create-keychain -p "$keychain_password" "$keychain" +security create-keychain -p $PW $RUNNER_TEMP/build.keychain +# Raise the keychain auto-lock timeout (6 hours) so it does not lock mid-build. +security set-keychain-settings -lut 21600 "$RUNNER_TEMP/build.keychain" # Mark the keychain as the system default so that a later signing step doesn't require # referencing the keychain by name. -security default-keychain -s "$keychain" +security default-keychain -s $RUNNER_TEMP/build.keychain # Unlock the keychain so that future operations can access the secrets without user interaction. -security unlock-keychain -p "$keychain_password" "$keychain" +security unlock-keychain -p $PW $RUNNER_TEMP/build.keychain -base64 -D <<<"$APPLE_APPLICATION_CERT" > "$RUNNER_TEMP/cert.p12" +# Decode the base64-encoded certificate secret into a .p12 file. +echo '${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }}' | base64 -d > $RUNNER_TEMP/cert.p12 # Import the certificate into the keychain so that a later signing step can use it. # `man security` snippet: # -k keychain Specify keychain into which item(s) will be imported. # -P passphrase Specify the unwrapping passphrase immediately. The default is to obtain a secure passphrase via GUI. # -T appPath Specify an application which may access the imported key (multiple -T options are allowed) -security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$APPLE_APPLICATION_CERT_PASSWORD" -T /usr/bin/codesign +security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}' -T /usr/bin/codesign # Enforce additional security requirements that only the applications used for signing can access the keychain. This allows for signing applications to access the keychain without user interaction. # The three values: @@ -245,18 +271,22 @@ security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$APPLE_APPLICATION_CE # Comma-separated partition list. See output of "security dump-keychain" for examples. # -k password Password for keychain # -s Match keys that can sign -security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain" -# Clean up the certificate so that it's not lying around for later jobs to leak. -rm "$RUNNER_TEMP/cert.p12" +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain ``` -When we execute `codesign --timestamp --options=runtime -s "${APPLE_DEVELOPER_ID?}" -v "$1"` in `./script/sign`, `codesign` inspects into the default keychain to find a certificate that matches the `APPLE_DEVELOPER_ID` environment variable. The `--timestamp` and `--options=runtime` flags are required for Notarization, described below. +> [!NOTE] +> The certificate and its password come from the `GATEWATCHER_DEVELOPER_ID_CERT` and `GATEWATCHER_DEVELOPER_ID_PASSWORD` secrets, replacing the previous `APPLE_APPLICATION_CERT` / `APPLE_APPLICATION_CERT_PASSWORD` secrets. The keychain is now named `build.keychain` (previously `buildagent.keychain`) and is passed explicitly to the signing scripts via the `KEYCHAIN` environment variable. + +When we execute `codesign --timestamp --options=runtime -s "${DEVELOPER_ID_CERT_IDENTIFIER?}" -v "$1"` in `./script/sign`, `codesign` searches the keychain for a certificate that matches the `DEVELOPER_ID_CERT_IDENTIFIER` environment variable (sourced from the `MAC_APP_SIGNING_IDENTITY` repository variable). The `--timestamp` and `--options=runtime` flags are required for Notarization, described below. + +> [!TIP] +> A `***: no identity found` failure from `codesign` means the value of `DEVELOPER_ID_CERT_IDENTIFIER` (i.e. `vars.MAC_APP_SIGNING_IDENTITY`) does not match any identity imported into the keychain. Run `security find-identity -v -p codesigning "$KEYCHAIN"` to list the available identities and their common names, then update the repository variable to match exactly. --- [Code signing certifies that a `gh` executable was created by GitHub](https://developer.apple.com/documentation/security/code-signing-services). On the other hand, [Notarization](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) is an additional security step upon which software is submitted to Apple for automated scanning. If passed, Apple generates a `ticket` that can be `stapled` to the software, and Apple's [Gatekeeper](https://support.apple.com/en-gb/guide/security/sec5599b66df/web) software is made aware of it. -When we execute `xcrun notarytool submit "$1" --apple-id "${APPLE_ID?}" --team-id "${APPLE_DEVELOPER_ID?}" --password "${APPLE_ID_PASSWORD?}"` in `./script/sign` no keychain access should be required as the `APPLE_ID_PASSWORD` environment variable is used to authenticate. +When we execute `xcrun notarytool submit "$1" --keychain $KEYCHAIN --keychain-profile "notarytool-password" --wait` in `./script/sign`, `notarytool` authenticates using the App Store Connect API key stored under the `notarytool-password` profile by the earlier `Configure notarization credentials` step. This replaces the previous Apple ID / app-specific password flow (`--apple-id` / `--team-id` / `--password`). ### [windows](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L147-L248) @@ -312,6 +342,7 @@ windows: DLIB_PATH: ${{ runner.temp }}\acs\bin\x64\Azure.CodeSigning.Dlib.dll METADATA_PATH: ${{ runner.temp }}\acs\metadata.json TAG_NAME: ${{ inputs.tag_name }} + DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }} run: script/release --local "$TAG_NAME" --platform windows - name: Set up MSBuild id: setupmsbuild @@ -353,6 +384,7 @@ windows: AZURE_TENANT_ID: ${{ secrets.SPN_GITHUB_CLI_SIGNING_TENANT_ID }} DLIB_PATH: ${{ runner.temp }}\acs\bin\x64\Azure.CodeSigning.Dlib.dll METADATA_PATH: ${{ runner.temp }}\acs\metadata.json + DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }} run: | Get-ChildItem -Path .\dist -Filter *.msi | ForEach-Object { .\script\sign.ps1 $_.FullName @@ -386,6 +418,8 @@ There are two levels of signing that occur in this job: * Signing of Go executables created by `GoReleaser` is performed in a [`GoReleaser` hook](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.goreleaser.yml#L43). * Signing of the MSI installers by executing `.\script\sign.ps1 $_.FullName` +Both the `Build release binaries` and `Sign .msi release binaries` steps set `DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }}`, which the signing scripts consult to skip Azure Code Signing outside of production deploys. The value is passed to both steps for consistency, even though the `Sign .msi release binaries` step is already guarded by `if: inputs.environment == 'production'`. + Signing of the Windows artifacts uses `signtool.exe` to request signing from [Azure HSM](https://azure.microsoft.com/en-us/products/azure-dedicated-hsm). This takes the following steps: Firstly, a package is downloaded that contains a DLL to allow `signtool.exe` to interact with Azure HSM. @@ -505,7 +539,7 @@ release: cp script/rpmmacros ~/.rpmmacros rpmsign --addsign dist/*.rpm - name: Attest release artifacts - if: inputs.environment == 'production' + if: inputs.environment == 'production' && !inputs.dry_run uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: "dist/gh_*" @@ -541,7 +575,7 @@ release: - name: Create the release env: # In non-production environments, the assets will not have been signed - DO_PUBLISH: ${{ inputs.environment == 'production' }} + DO_PUBLISH: ${{ inputs.environment == 'production' && !inputs.dry_run }} TAG_NAME: ${{ inputs.tag_name }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -564,7 +598,7 @@ release: script/label-assets dist/gh_* | xargs $guard gh release create "${release_args[@]}" -- - name: Publish site env: - DO_PUBLISH: ${{ inputs.environment == 'production' && !contains(inputs.tag_name, '-') }} + DO_PUBLISH: ${{ inputs.environment == 'production' && !contains(inputs.tag_name, '-') && !inputs.dry_run }} TAG_NAME: ${{ inputs.tag_name }} GIT_COMMITTER_NAME: cli automation GIT_AUTHOR_NAME: cli automation @@ -589,6 +623,8 @@ The following sections are not strictly in the same order as the workflow but in A git commit is created in the `cli.github.com` site repository containing the contents of the CLI Manual uploaded by the [`linux`](#linux) job. This is not pushed until the package repository artifacts are set up later. +The `cli.github.com` repository is checked out using a short-lived GitHub App installation token rather than a long-lived PAT. The `Generate site deploy token` step (which only runs when `inputs.environment == 'production'`) uses [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) with the `SITE_DEPLOY_APP_CLIENT_ID` / `SITE_DEPLOY_APP_PRIVATE_KEY` secrets to mint a token scoped to the `github/cli.github.com` repository, replacing the previous `SITE_DEPLOY_PAT` secret. In non-production environments the token step is skipped and the checkout falls back to `github.token` for anonymous read access (`token: ${{ steps.site-deploy-token.outputs.token || github.token }}`); pushing to the site is gated separately (see [Publishing behaviour and dry runs](#dry-run)). + ### Site Package Repositories The `cli.github.com` website hosts RPM and Debian package repositories to support the [official sources installation instructions](https://github.com/cli/cli/blob/trunk/docs/install_linux.md#official-sources). In order to provide a secure installation method, artifacts in these repositories are signed by a GPG key, which must be loaded into `gpg` for use in later steps. Comments have been added to provide clarity to the script: @@ -624,6 +660,8 @@ The `.deb` files uploaded by the [`linux`](#linux) job are iterated per Debian r [Attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds) are created for each of the release artifacts. For an example see: https://github.com/cli/cli/attestations/4920729 +Attestation creation is skipped on dry runs (`if: inputs.environment == 'production' && !inputs.dry_run`), since attestations are externally visible provenance records that should only be produced for real releases. + ### Publish Release After all release artifacts have been created, and signed, there are a number of steps taken to make them available to our users. @@ -644,7 +682,7 @@ $ gh release create v1.2.3 '/path/to/asset.zip#My display label' #### Site -In previous steps, a git commit was made for the manual, and files had moved into place for the RPM and Debian package repositories. The package repository structure is committed and pushed, which kicks off a deployment workflow in site repository. +In previous steps, a git commit was made for the manual, and files had moved into place for the RPM and Debian package repositories. The package repository structure is committed and pushed, which kicks off a deployment workflow in site repository. The push only happens when `DO_PUBLISH` is `true` (production, non-prerelease tag, and not a dry run); otherwise the step prints the pending commits and diff for inspection instead of pushing. Occasionally, the repository can become unwieldy due to hosting so many large binary artifacts. Instructions can be found in the README for that repository. @@ -654,6 +692,18 @@ Historically, we used [`mislav/bump-homebrew-formula-action`](https://github.com However, since this required a legacy PAT token to open a PR between these repositories, it was deemed too much risk for our security. As such, we now rely on [Homebrew's autobump](https://docs.brew.sh/Autobump). +### Publishing behaviour and dry runs + +The `dry_run` input (a boolean that defaults to `true` on the `workflow_dispatch` form) provides a final safety valve on top of the `environment` guard. When `dry_run` is `true`, the workflow still performs a full production build, including code signing, notarization and package repository generation, but skips every step that mutates externally visible state: + +| Step | Guard | +| --- | --- | +| [Attest release artifacts](#attest-artifacts) | `inputs.environment == 'production' && !inputs.dry_run` | +| Create the release (`gh release create`) | `DO_PUBLISH: inputs.environment == 'production' && !inputs.dry_run` | +| [Publish site](#site) (push to `cli.github.com`) | `DO_PUBLISH: inputs.environment == 'production' && !contains(inputs.tag_name, '-') && !inputs.dry_run` | + +The `Create the release` and `Publish site` steps consult their `DO_PUBLISH` environment variable: when it is `false` the release command is prefixed with `echo` (so the `gh release create` invocation is only printed, not executed) and the site push is replaced with a `git log` / `git diff` of the pending changes. This means a dry run exercises the entire pipeline end-to-end, making it a safe way to validate signing and packaging changes without creating a GitHub Release, publishing attestations, or pushing to the site repository. + ## Deepest Dive ### How script/release works From 070ed9bd32b97357079e62ffb280ac0ad08a3620 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 15:15:19 +0200 Subject: [PATCH 22/33] Forward dry_run to deployment workflow from script/release The deployment workflow's dry_run input defaults to true so manual UI dispatches are safe by default. script/release, however, is used to initiate real releases, so it now passes -f dry_run=false and exposes an opt-in --dry-run flag. Without this, releases triggered via script/release silently became no-ops (no attestation, no GitHub Release, no site push). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- script/release | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/script/release b/script/release index a3f19016d7d..47646e8c991 100755 --- a/script/release +++ b/script/release @@ -4,7 +4,7 @@ set -e print_help() { cat < [--platform {linux|macos|windows}] [--branch ] + script/release [--staging] [--dry-run] [--platform {linux|macos|windows}] [--branch ] To build staging binaries from the current branch: script/release --current [--platform {linux|macos|windows}] @@ -25,6 +25,7 @@ do_push="" platform="" branch="trunk" deploy_env="production" +dry_run="false" while [ $# -gt 0 ]; do case "$1" in @@ -48,6 +49,10 @@ while [ $# -gt 0 ]; do deploy_env="staging" shift 1 ;; + --dry-run ) + dry_run="true" + shift 1 + ;; --current ) deploy_env="staging" tag_name="$(git describe --tags --abbrev=0)" @@ -74,7 +79,7 @@ announce() { } trigger_deployment() { - announce gh workflow -R cli/cli run deployment.yml --ref "$branch" -f tag_name="$tag_name" -f environment="$deploy_env" + announce gh workflow -R cli/cli run deployment.yml --ref "$branch" -f tag_name="$tag_name" -f environment="$deploy_env" -f dry_run="$dry_run" } build_local() { From 7441b077ac1ed0c5c83fbe0e566e2b59610a5e02 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 15:17:44 +0200 Subject: [PATCH 23/33] Remove ref input from deployment workflow The ref input was added in anticipation of calling this workflow via workflow_call from another repo. Since it remains a workflow_dispatch workflow, drop the input and let actions/checkout default to the ref that triggered the dispatch (the --ref passed to gh workflow run). This also fixes script/release --branch, which previously selected the workflow file ref but still built trunk because inputs.ref defaulted to trunk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 13 ------------- docs/release-process-deep-dive.md | 3 +-- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index d0bd8687ecb..d1ca9a120c4 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -13,11 +13,6 @@ permissions: on: workflow_dispatch: inputs: - ref: - required: true - type: string - description: "The ref to checkout for the release." - default: "trunk" tag_name: required: true type: string @@ -60,8 +55,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - ref: ${{ inputs.ref }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -106,8 +99,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - ref: ${{ inputs.ref }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -208,8 +199,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - ref: ${{ inputs.ref }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -328,8 +317,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - ref: ${{ inputs.ref }} - name: Merge built artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: Generate site deploy token diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index 43e2a55f59d..e3984aefbf0 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -28,7 +28,6 @@ Although this workflow is used to do our production releases for Linux, MacOS an * Many sections are guarded by `if: inputs.environment == 'production'`. These guards protect sections that require secrets (e.g. signing) or that result in mutations (e.g. creating a GitHub release). `./script/release` accepts the `--staging` flag for this purpose. This differs from the previous bullet point as some steps in the [release job](#release) print debug information such as [`git` diffs](https://github.com/cli/cli/blob/5d2eadef8cccf2671f68aad05cd93215a4c01b48/.github/workflows/deployment.yml#L380-L384). * The workflow can be triggered with `inputs.dry_run` set to `true` (the default for the `workflow_dispatch` form). Unlike `inputs.environment`, a dry run still exercises the `production` signing and packaging steps, but it does **not** publish anything: creating GitHub attestations, creating the GitHub Release, and pushing to the `cli.github.com` site repository are all skipped. This makes it possible to validate a full production build, including signing, without mutating anything externally visible. See [Publishing behaviour and dry runs](#dry-run) for details. * The workflow can be triggered for only `linux`, `MacOS` or `Windows` which allows for debugging single jobs. This is not exposed via `./script/release`. The [release job](#release) should not run in this case as it [requires all OS specific builds.](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L252) - * The workflow accepts an `inputs.ref` input (defaulting to `trunk`) that controls the git ref checked out by every job, allowing a release to be built from a branch or specific commit. ## [validate-tag-name](https://github.com/cli/cli/blob/537a22228cd6b42b740d7f1c09f47c45bb1dab30/.github/workflows/deployment.yml#L31-L39) @@ -54,7 +53,7 @@ The purpose of this job is to [prevent incorrectly tagged releases](https://gith ## [OS Builds](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L40-L248) -After validating the tag name, the workflow parallelises across `ubuntu`, `macos` and `windows` runners. The primary purpose of these jobs is to build and sign release artifacts. These artifacts are made available to the `release` job via `actions/upload-artifact` and `actions/download-artifact` respectively. Each of these jobs (as well as `release`) checks out the ref specified by `inputs.ref` and sets `timeout-minutes: 20` so a hung build (for example, a code-signing step waiting on a remote service) fails fast rather than consuming the full default job timeout. +After validating the tag name, the workflow parallelises across `ubuntu`, `macos` and `windows` runners. The primary purpose of these jobs is to build and sign release artifacts. These artifacts are made available to the `release` job via `actions/upload-artifact` and `actions/download-artifact` respectively. Each of these jobs (as well as `release`) checks out the ref that triggered the `workflow_dispatch` (i.e. the `--ref` passed to `gh workflow run`, which `./script/release` sets from `--branch`) and sets `timeout-minutes: 20` so a hung build (for example, a code-signing step waiting on a remote service) fails fast rather than consuming the full default job timeout. ### [linux](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L40-L73) From 7a6ed735cf35f105d28102bc3caf3c9e45979baa Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 15:19:36 +0200 Subject: [PATCH 24/33] Pass macOS signing secrets via env instead of inline interpolation Interpolating ${{ secrets.* }} directly into the run: script meant a certificate password containing a single quote (or other shell metacharacters) would break quoting and could inject commands. Map the cert and password into the step env and reference them as quoted shell variables instead, and remove the temporary .p12 after import. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 8 ++++++-- docs/release-process-deep-dive.md | 22 ++++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index d1ca9a120c4..c21c150c1dc 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -107,6 +107,9 @@ jobs: - name: Install code signing certificate if: inputs.environment == 'production' shell: bash + env: + DEVELOPER_ID_CERT: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }} + DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }} run: | # create a keychain for the certificate PW=pwd.${{ github.run_number }} @@ -116,9 +119,10 @@ jobs: security unlock-keychain -p $PW $RUNNER_TEMP/build.keychain # import the certificate - echo '${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }}' | base64 -d > $RUNNER_TEMP/cert.p12 - security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}' -T /usr/bin/codesign + base64 -d <<<"$DEVELOPER_ID_CERT" > $RUNNER_TEMP/cert.p12 + security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain + rm $RUNNER_TEMP/cert.p12 - name: Add App Store Connect API key to keychain if: inputs.environment == 'production' diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index e3984aefbf0..6ccda8832d7 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -127,6 +127,9 @@ There is no signing of linux artifacts in this job. See the [release job](#relea - name: Install code signing certificate if: inputs.environment == 'production' shell: bash + env: + DEVELOPER_ID_CERT: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }} + DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }} run: | # create a keychain for the certificate PW=pwd.${{ github.run_number }} @@ -136,9 +139,10 @@ There is no signing of linux artifacts in this job. See the [release job](#relea security unlock-keychain -p $PW $RUNNER_TEMP/build.keychain # import the certificate - echo '${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }}' | base64 -d > $RUNNER_TEMP/cert.p12 - security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}' -T /usr/bin/codesign + base64 -d <<<"$DEVELOPER_ID_CERT" > $RUNNER_TEMP/cert.p12 + security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain + rm $RUNNER_TEMP/cert.p12 - name: Add App Store Connect API key to keychain if: inputs.environment == 'production' uses: nodeselector/setup-apple-codesign@ab275d0f6fb63ef9e20b12b42ea0d567f935723c @@ -246,15 +250,19 @@ security default-keychain -s $RUNNER_TEMP/build.keychain # Unlock the keychain so that future operations can access the secrets without user interaction. security unlock-keychain -p $PW $RUNNER_TEMP/build.keychain -# Decode the base64-encoded certificate secret into a .p12 file. -echo '${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }}' | base64 -d > $RUNNER_TEMP/cert.p12 +# Decode the base64-encoded certificate secret into a .p12 file. The +# certificate and password are passed in via the DEVELOPER_ID_CERT and +# DEVELOPER_ID_CERT_PASSWORD env vars (mapped from secrets) rather than +# interpolated into the script, so a password containing shell +# metacharacters cannot break quoting or be injected. +base64 -d <<<"$DEVELOPER_ID_CERT" > $RUNNER_TEMP/cert.p12 # Import the certificate into the keychain so that a later signing step can use it. # `man security` snippet: # -k keychain Specify keychain into which item(s) will be imported. # -P passphrase Specify the unwrapping passphrase immediately. The default is to obtain a secure passphrase via GUI. # -T appPath Specify an application which may access the imported key (multiple -T options are allowed) -security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}' -T /usr/bin/codesign +security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign # Enforce additional security requirements that only the applications used for signing can access the keychain. This allows for signing applications to access the keychain without user interaction. # The three values: @@ -271,10 +279,12 @@ security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P '${{ sec # -k password Password for keychain # -s Match keys that can sign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain +# Clean up the certificate so that it's not lying around for later steps to leak. +rm $RUNNER_TEMP/cert.p12 ``` > [!NOTE] -> The certificate and its password come from the `GATEWATCHER_DEVELOPER_ID_CERT` and `GATEWATCHER_DEVELOPER_ID_PASSWORD` secrets, replacing the previous `APPLE_APPLICATION_CERT` / `APPLE_APPLICATION_CERT_PASSWORD` secrets. The keychain is now named `build.keychain` (previously `buildagent.keychain`) and is passed explicitly to the signing scripts via the `KEYCHAIN` environment variable. +> The certificate and its password come from the `GATEWATCHER_DEVELOPER_ID_CERT` and `GATEWATCHER_DEVELOPER_ID_PASSWORD` secrets, replacing the previous `APPLE_APPLICATION_CERT` / `APPLE_APPLICATION_CERT_PASSWORD` secrets. They are mapped into the step's `env:` and referenced as `"$DEVELOPER_ID_CERT"` / `"$DEVELOPER_ID_CERT_PASSWORD"` rather than being interpolated directly into the `run:` script, so a password containing shell metacharacters cannot break quoting or inject commands. The keychain is now named `build.keychain` (previously `buildagent.keychain`) and is passed explicitly to the signing scripts via the `KEYCHAIN` environment variable. When we execute `codesign --timestamp --options=runtime -s "${DEVELOPER_ID_CERT_IDENTIFIER?}" -v "$1"` in `./script/sign`, `codesign` searches the keychain for a certificate that matches the `DEVELOPER_ID_CERT_IDENTIFIER` environment variable (sourced from the `MAC_APP_SIGNING_IDENTITY` repository variable). The `--timestamp` and `--options=runtime` flags are required for Notarization, described below. From b63b8117c76358016c69cfd31656596e1987a580 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 15:23:40 +0200 Subject: [PATCH 25/33] Gate macOS signing on DO_SIGN_ARTIFACTS Mirror the Windows job: script/sign now skips codesign/notarization unless DO_SIGN_ARTIFACTS is set to a non-false value, and the macOS Build and Notarize steps set it to production-only. Previously, staging macOS builds would run codesign against a keychain that was never provisioned (the cert-install step is production-only) whenever MAC_APP_SIGNING_IDENTITY was defined at repository scope, breaking non-production builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 2 ++ docs/release-process-deep-dive.md | 4 ++++ script/sign | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index c21c150c1dc..b7659738dbe 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -162,12 +162,14 @@ jobs: TAG_NAME: ${{ inputs.tag_name }} KEYCHAIN: ${{ runner.temp }}/build.keychain DEVELOPER_ID_CERT_IDENTIFIER: ${{ vars.MAC_APP_SIGNING_IDENTITY }} + DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }} run: script/release --local "$TAG_NAME" --platform macos - name: Notarize macOS archives if: inputs.environment == 'production' env: DEVELOPER_ID_CERT_IDENTIFIER: ${{ vars.MAC_APP_SIGNING_IDENTITY }} KEYCHAIN: ${{ runner.temp }}/build.keychain + DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }} # Technically redundant given the step guard above, but kept for consistency with the build step. run: | shopt -s failglob script/sign dist/gh_*_macOS_*.zip diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index 6ccda8832d7..fa251c64cc9 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -171,12 +171,14 @@ There is no signing of linux artifacts in this job. See the [release job](#relea TAG_NAME: ${{ inputs.tag_name }} KEYCHAIN: ${{ runner.temp }}/build.keychain DEVELOPER_ID_CERT_IDENTIFIER: ${{ vars.MAC_APP_SIGNING_IDENTITY }} + DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }} run: script/release --local "$TAG_NAME" --platform macos - name: Notarize macOS archives if: inputs.environment == 'production' env: DEVELOPER_ID_CERT_IDENTIFIER: ${{ vars.MAC_APP_SIGNING_IDENTITY }} KEYCHAIN: ${{ runner.temp }}/build.keychain + DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }} run: | shopt -s failglob script/sign dist/gh_*_macOS_*.zip @@ -288,6 +290,8 @@ rm $RUNNER_TEMP/cert.p12 When we execute `codesign --timestamp --options=runtime -s "${DEVELOPER_ID_CERT_IDENTIFIER?}" -v "$1"` in `./script/sign`, `codesign` searches the keychain for a certificate that matches the `DEVELOPER_ID_CERT_IDENTIFIER` environment variable (sourced from the `MAC_APP_SIGNING_IDENTITY` repository variable). The `--timestamp` and `--options=runtime` flags are required for Notarization, described below. +`./script/sign` only signs when `DO_SIGN_ARTIFACTS` is set to a value other than `false`; the `Build release binaries` and `Notarize macOS archives` steps set `DO_SIGN_ARTIFACTS: ${{ inputs.environment == 'production' }}` (mirroring the Windows job). This ensures non-production (staging) macOS builds skip signing gracefully rather than failing when `codesign` runs against a keychain that was never provisioned, regardless of whether `MAC_APP_SIGNING_IDENTITY` happens to be defined at repository scope. + > [!TIP] > A `***: no identity found` failure from `codesign` means the value of `DEVELOPER_ID_CERT_IDENTIFIER` (i.e. `vars.MAC_APP_SIGNING_IDENTITY`) does not match any identity imported into the keychain. Run `security find-identity -v -p codesigning "$KEYCHAIN"` to list the available identities and their common names, then update the repository variable to match exactly. diff --git a/script/sign b/script/sign index 569fbc770c6..15e02383cbb 100755 --- a/script/sign +++ b/script/sign @@ -6,6 +6,11 @@ set -e sign_macos() { + if [[ -z "$DO_SIGN_ARTIFACTS" || "$DO_SIGN_ARTIFACTS" == "false" ]]; then + echo "skipping macOS code-signing; DO_SIGN_ARTIFACTS not set or false" >&2 + return 0 + fi + if [[ -z "$DEVELOPER_ID_CERT_IDENTIFIER" ]]; then echo "skipping macOS code-signing; DEVELOPER_ID_CERT_IDENTIFIER not set" >&2 return 0 From 64840c740b828ddf841312d0bba790e49f1ede38 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 15:36:37 +0200 Subject: [PATCH 26/33] Mark dry runs in the workflow run name Append a '(dry run)' suffix to run-name when inputs.dry_run is true so dry runs are distinguishable from real deployments in the Actions UI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 2 +- docs/release-process-deep-dive.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index b7659738dbe..228641726d5 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -1,5 +1,5 @@ name: Deployment -run-name: ${{ inputs.tag_name }} / ${{ inputs.environment }} +run-name: ${{ inputs.tag_name }} / ${{ inputs.environment }}${{ inputs.dry_run == true && ' (dry run)' || '' }} concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index fa251c64cc9..0ab4ad1a4aa 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -717,6 +717,8 @@ The `dry_run` input (a boolean that defaults to `true` on the `workflow_dispatch The `Create the release` and `Publish site` steps consult their `DO_PUBLISH` environment variable: when it is `false` the release command is prefixed with `echo` (so the `gh release create` invocation is only printed, not executed) and the site push is replaced with a `git log` / `git diff` of the pending changes. This means a dry run exercises the entire pipeline end-to-end, making it a safe way to validate signing and packaging changes without creating a GitHub Release, publishing attestations, or pushing to the site repository. +To make dry runs easy to spot in the Actions UI, the workflow's `run-name` appends a `(dry run)` suffix when `inputs.dry_run` is `true` (`run-name: ${{ inputs.tag_name }} / ${{ inputs.environment }}${{ inputs.dry_run == true && ' (dry run)' || '' }}`). + ## Deepest Dive ### How script/release works From a79c060571c65e540414af4bef7c90e83d8d6360 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 2 Jul 2026 15:56:55 +0200 Subject: [PATCH 27/33] Revert to missing var --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 228641726d5..a3be68a2302 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -182,7 +182,7 @@ jobs: if: inputs.environment == 'production' env: TAG_NAME: ${{ inputs.tag_name }} - APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.MAC_APP_SIGNING_IDENTITY }} + APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.APPLE_DEVELOPER_INSTALLER_ID }} run: | shopt -s failglob script/pkgmacos "$TAG_NAME" From ed04ff24848581ae56e7581145ec283b06e88ce5 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 3 Jul 2026 08:27:50 +0200 Subject: [PATCH 28/33] Gate publishing the site on the production environment --- .github/workflows/deployment.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 870fbe2e092..0b255679c7c 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -335,16 +335,15 @@ jobs: owner: github repositories: cli.github.com - name: Checkout documentation site + if: ${{ inputs.environment == 'production' }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: github/cli.github.com path: site fetch-depth: 0 - # In non-production environments the App token step is skipped, so - # fall back to github.token for anonymous read access to the public - # cli.github.com repo (push is gated separately on DO_PUBLISH). - token: ${{ steps.site-deploy-token.outputs.token || github.token }} + token: ${{ steps.site-deploy-token.outputs.token}} - name: Update site man pages + if: ${{ inputs.environment == 'production' }} env: GIT_COMMITTER_NAME: cli automation GIT_AUTHOR_NAME: cli automation @@ -394,6 +393,7 @@ jobs: subject-path: "dist/gh_*" create-storage-record: false # (default: true) - name: Run createrepo + if: ${{ inputs.environment == 'production' }} env: GPG_SIGN: ${{ inputs.environment == 'production' }} run: | @@ -405,6 +405,7 @@ jobs: [ "$GPG_SIGN" = "false" ] || gpg --yes --detach-sign --armor repodata/repomd.xml popd - name: Run reprepro + if: ${{ inputs.environment == 'production' }} env: GPG_SIGN: ${{ inputs.environment == 'production' }} # We are no longer adding to the distribution list. @@ -447,6 +448,7 @@ jobs: [ "$DO_PUBLISH" = "false" ] || guard="" script/label-assets dist/gh_* | xargs $guard gh release create "${release_args[@]}" -- - name: Publish site + if: ${{ inputs.environment == 'production' }} env: DO_PUBLISH: ${{ inputs.environment == 'production' && !contains(inputs.tag_name, '-') && !inputs.dry_run }} TAG_NAME: ${{ inputs.tag_name }} From 1b7da92b1b63ac1af6549d5d253ce39b4507ddfe Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 3 Jul 2026 08:39:16 +0200 Subject: [PATCH 29/33] Fix pkg installer var in deep-dive code snippet The workflow was reverted to source APPLE_DEVELOPER_INSTALLER_ID from vars.APPLE_DEVELOPER_INSTALLER_ID, but the docs snippet still showed vars.MAC_APP_SIGNING_IDENTITY. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-process-deep-dive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index 4fe03e14ae0..cd7766e06d3 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -191,7 +191,7 @@ There is no signing of linux artifacts in this job. See the [release job](#relea if: inputs.environment == 'production' env: TAG_NAME: ${{ inputs.tag_name }} - APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.MAC_APP_SIGNING_IDENTITY }} + APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.APPLE_DEVELOPER_INSTALLER_ID }} run: | shopt -s failglob script/pkgmacos "$TAG_NAME" From ee7fd9d0e09f5531ab6c7fa1c63439ce7316beda Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 3 Jul 2026 08:39:29 +0200 Subject: [PATCH 30/33] Correct pkg signing NOTE in deep-dive The note claimed the step now passes vars.MAC_APP_SIGNING_IDENTITY, but the workflow still passes the (unset) vars.APPLE_DEVELOPER_INSTALLER_ID. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-process-deep-dive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index cd7766e06d3..94d58ceab8b 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -226,7 +226,7 @@ There are three levels of "signing" that occur in this job: > Although the job title is `Build & notarize universal macOS pkg installer`, the [`productbuild` docs](https://www.unix.com/man_page/osx/1/productbuild/) only refer to signing, thus notarization may not be the correct term here. > [!NOTE] -> Historically the `.pkg` installer was never actually signed because `${{ vars.APPLE_DEVELOPER_INSTALLER_ID }}` was [never set](https://github.com/cli/cli/actions/runs/13271193192/job/37050749548#step:9:11). The `Build & notarize universal macOS pkg installer` step now passes `APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.MAC_APP_SIGNING_IDENTITY }}`, so `productbuild` signing runs when that repository variable is populated. +> Historically the `.pkg` installer was never actually signed because `${{ vars.APPLE_DEVELOPER_INSTALLER_ID }}` was [never set](https://github.com/cli/cli/actions/runs/13271193192/job/37050749548#step:9:11). The `Build & notarize universal macOS pkg installer` step still passes `APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.APPLE_DEVELOPER_INSTALLER_ID }}`, so `productbuild` signing will run once that repository variable is populated. Signing of MacOS artifacts uses `codesign` and notarization uses `xcrun notarytool`, which submits the artifact to the Apple servers for additional checks. From 7822e916b7156853ca55e2eb2ce1d34329238ce6 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 3 Jul 2026 08:39:42 +0200 Subject: [PATCH 31/33] Quote $KEYCHAIN in notarytool submit invocation Prevents word-splitting/globbing if the keychain path ever contains spaces or glob characters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-process-deep-dive.md | 2 +- script/sign | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index 94d58ceab8b..cc3e3f802f0 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -299,7 +299,7 @@ When we execute `codesign --timestamp --options=runtime -s "${DEVELOPER_ID_CERT_ [Code signing certifies that a `gh` executable was created by GitHub](https://developer.apple.com/documentation/security/code-signing-services). On the other hand, [Notarization](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) is an additional security step upon which software is submitted to Apple for automated scanning. If passed, Apple generates a `ticket` that can be `stapled` to the software, and Apple's [Gatekeeper](https://support.apple.com/en-gb/guide/security/sec5599b66df/web) software is made aware of it. -When we execute `xcrun notarytool submit "$1" --keychain $KEYCHAIN --keychain-profile "notarytool-password" --wait` in `./script/sign`, `notarytool` authenticates using the App Store Connect API key stored under the `notarytool-password` profile by the earlier `Configure notarization credentials` step. This replaces the previous Apple ID / app-specific password flow (`--apple-id` / `--team-id` / `--password`). +When we execute `xcrun notarytool submit "$1" --keychain "$KEYCHAIN" --keychain-profile "notarytool-password" --wait` in `./script/sign`, `notarytool` authenticates using the App Store Connect API key stored under the `notarytool-password` profile by the earlier `Configure notarization credentials` step. This replaces the previous Apple ID / app-specific password flow (`--apple-id` / `--team-id` / `--password`). ### [windows](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L147-L248) diff --git a/script/sign b/script/sign index 15e02383cbb..e5d6049412e 100755 --- a/script/sign +++ b/script/sign @@ -22,7 +22,7 @@ sign_macos() { fi if [[ $1 == *.zip ]]; then - xcrun notarytool submit "$1" --keychain $KEYCHAIN --keychain-profile "notarytool-password" --wait + xcrun notarytool submit "$1" --keychain "$KEYCHAIN" --keychain-profile "notarytool-password" --wait else codesign --timestamp --options=runtime -s "${DEVELOPER_ID_CERT_IDENTIFIER?}" -v "$1" fi From 9cde87c7dd6c1808256e3af8b0c7ab8f7f460011 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 3 Jul 2026 13:58:46 +0200 Subject: [PATCH 32/33] Apply suggestions from code review Co-authored-by: Babak K. Shandiz --- .github/workflows/deployment.yml | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 0b255679c7c..369d5481535 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -20,13 +20,13 @@ on: environment: default: production type: environment - description: "The deployment environment..." + description: "The deployment environment." platforms: default: "linux,macos,windows" type: string description: "Comma-separated list of platforms to build." release: - description: "Whether to create a GitHub Release" + description: "Whether to run the final release job. the dry_run flag still blocks final submissions." type: boolean default: true dry_run: @@ -113,16 +113,16 @@ jobs: run: | # create a keychain for the certificate PW=pwd.${{ github.run_number }} - security create-keychain -p $PW $RUNNER_TEMP/build.keychain + security create-keychain -p $PW "$RUNNER_TEMP/build.keychain" security set-keychain-settings -lut 21600 "$RUNNER_TEMP/build.keychain" - security default-keychain -s $RUNNER_TEMP/build.keychain - security unlock-keychain -p $PW $RUNNER_TEMP/build.keychain + security default-keychain -s "$RUNNER_TEMP/build.keychain" + security unlock-keychain -p $PW "$RUNNER_TEMP/build.keychain" # import the certificate - base64 -d <<<"$DEVELOPER_ID_CERT" > $RUNNER_TEMP/cert.p12 - security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain - rm $RUNNER_TEMP/cert.p12 + base64 -d <<< "$DEVELOPER_ID_CERT" > "$RUNNER_TEMP/cert.p12" + security import "$RUNNER_TEMP/cert.p12" -k "$RUNNER_TEMP/build.keychain" -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW "$RUNNER_TEMP/build.keychain" + rm "$RUNNER_TEMP/cert.p12" - name: Add App Store Connect API key to keychain if: inputs.environment == 'production' @@ -142,7 +142,7 @@ jobs: --key "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-key-path }}" \ --key-id "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-key-id }}" \ --issuer "${{ steps.setup-apple-codesign.outputs.app-store-connect-api-key-issuer-id }}" \ - --keychain $RUNNER_TEMP/build.keychain + --keychain "$RUNNER_TEMP/build.keychain" - name: Install GoReleaser uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 @@ -394,27 +394,22 @@ jobs: create-storage-record: false # (default: true) - name: Run createrepo if: ${{ inputs.environment == 'production' }} - env: - GPG_SIGN: ${{ inputs.environment == 'production' }} run: | mkdir -p site/packages/rpm cp dist/*.rpm site/packages/rpm/ ./script/createrepo.sh cp -r dist/repodata site/packages/rpm/ pushd site/packages/rpm - [ "$GPG_SIGN" = "false" ] || gpg --yes --detach-sign --armor repodata/repomd.xml + gpg --yes --detach-sign --armor repodata/repomd.xml popd - name: Run reprepro if: ${{ inputs.environment == 'production' }} - env: - GPG_SIGN: ${{ inputs.environment == 'production' }} # We are no longer adding to the distribution list. # All apt distributions should use "stable" according to our install documentation. # In the future we will remove legacy distributions listed here. RELEASES: "cosmic eoan disco groovy focal stable oldstable testing sid unstable buster bullseye stretch jessie bionic trusty precise xenial hirsute impish kali-rolling" run: | mkdir -p upload - [ "$GPG_SIGN" = "true" ] || sed -i.bak '/^SignWith:/d' script/distributions for release in $RELEASES; do for file in dist/*.deb; do reprepro --confdir="+b/script" includedeb "$release" "$file" From 57a479ffea7a67a0d93908ae46ef049d079dfde3 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 3 Jul 2026 14:07:13 +0200 Subject: [PATCH 33/33] Update deep-dive doc for final release workflow state Bring the release deep-dive in line with the final branch state: site steps gated on production, createrepo/reprepro gate on environment instead of GPG_SIGN, dropped github.token fallback, quoted keychain paths, and documented the script/release --dry-run default. Also restore the reprepro env: key that was dropped when GPG_SIGN was removed, which had left the workflow YAML invalid. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment.yml | 1 + docs/release-process-deep-dive.md | 34 +++++++++++++++---------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 369d5481535..efa1a8b1b10 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -404,6 +404,7 @@ jobs: popd - name: Run reprepro if: ${{ inputs.environment == 'production' }} + env: # We are no longer adding to the distribution list. # All apt distributions should use "stable" according to our install documentation. # In the future we will remove legacy distributions listed here. diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index cc3e3f802f0..cd871e20e6a 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -243,28 +243,28 @@ In order to perform signing, a keychain must be configured with the signing cert PW=pwd.${{ github.run_number }} # Create a new keychain for credentials to be stored in. -security create-keychain -p $PW $RUNNER_TEMP/build.keychain +security create-keychain -p $PW "$RUNNER_TEMP/build.keychain" # Raise the keychain auto-lock timeout (6 hours) so it does not lock mid-build. security set-keychain-settings -lut 21600 "$RUNNER_TEMP/build.keychain" # Mark the keychain as the system default so that a later signing step doesn't require # referencing the keychain by name. -security default-keychain -s $RUNNER_TEMP/build.keychain +security default-keychain -s "$RUNNER_TEMP/build.keychain" # Unlock the keychain so that future operations can access the secrets without user interaction. -security unlock-keychain -p $PW $RUNNER_TEMP/build.keychain +security unlock-keychain -p $PW "$RUNNER_TEMP/build.keychain" # Decode the base64-encoded certificate secret into a .p12 file. The # certificate and password are passed in via the DEVELOPER_ID_CERT and # DEVELOPER_ID_CERT_PASSWORD env vars (mapped from secrets) rather than # interpolated into the script, so a password containing shell # metacharacters cannot break quoting or be injected. -base64 -d <<<"$DEVELOPER_ID_CERT" > $RUNNER_TEMP/cert.p12 +base64 -d <<<"$DEVELOPER_ID_CERT" > "$RUNNER_TEMP/cert.p12" # Import the certificate into the keychain so that a later signing step can use it. # `man security` snippet: # -k keychain Specify keychain into which item(s) will be imported. # -P passphrase Specify the unwrapping passphrase immediately. The default is to obtain a secure passphrase via GUI. # -T appPath Specify an application which may access the imported key (multiple -T options are allowed) -security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign +security import "$RUNNER_TEMP/cert.p12" -k "$RUNNER_TEMP/build.keychain" -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign # Enforce additional security requirements that only the applications used for signing can access the keychain. This allows for signing applications to access the keychain without user interaction. # The three values: @@ -280,9 +280,9 @@ security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/build.keychain -P "$DEVELO # Comma-separated partition list. See output of "security dump-keychain" for examples. # -k password Password for keychain # -s Match keys that can sign -security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW $RUNNER_TEMP/build.keychain +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $PW "$RUNNER_TEMP/build.keychain" # Clean up the certificate so that it's not lying around for later steps to leak. -rm $RUNNER_TEMP/cert.p12 +rm "$RUNNER_TEMP/cert.p12" ``` > [!NOTE] @@ -499,16 +499,15 @@ release: owner: github repositories: cli.github.com - name: Checkout documentation site + if: ${{ inputs.environment == 'production' }} uses: actions/checkout@v4 with: repository: github/cli.github.com path: site fetch-depth: 0 - # In non-production environments the App token step is skipped, so - # fall back to github.token for anonymous read access to the public - # cli.github.com repo (push is gated separately on DO_PUBLISH). - token: ${{ steps.site-deploy-token.outputs.token || github.token }} + token: ${{ steps.site-deploy-token.outputs.token }} - name: Update site man pages + if: ${{ inputs.environment == 'production' }} env: GIT_COMMITTER_NAME: cli automation GIT_AUTHOR_NAME: cli automation @@ -557,26 +556,24 @@ release: with: subject-path: "dist/gh_*" - name: Run createrepo - env: - GPG_SIGN: ${{ inputs.environment == 'production' }} + if: ${{ inputs.environment == 'production' }} run: | mkdir -p site/packages/rpm cp dist/*.rpm site/packages/rpm/ ./script/createrepo.sh cp -r dist/repodata site/packages/rpm/ pushd site/packages/rpm - [ "$GPG_SIGN" = "false" ] || gpg --yes --detach-sign --armor repodata/repomd.xml + gpg --yes --detach-sign --armor repodata/repomd.xml popd - name: Run reprepro + if: ${{ inputs.environment == 'production' }} env: - GPG_SIGN: ${{ inputs.environment == 'production' }} # We are no longer adding to the distribution list. # All apt distributions should use "stable" according to our install documentation. # In the future we will remove legacy distributions listed here. RELEASES: "cosmic eoan disco groovy focal stable oldstable testing sid unstable buster bullseye stretch jessie bionic trusty precise xenial hirsute impish kali-rolling" run: | mkdir -p upload - [ "$GPG_SIGN" = "true" ] || sed -i.bak '/^SignWith:/d' script/distributions for release in $RELEASES; do for file in dist/*.deb; do reprepro --confdir="+b/script" includedeb "$release" "$file" @@ -636,7 +633,7 @@ The following sections are not strictly in the same order as the workflow but in A git commit is created in the `cli.github.com` site repository containing the contents of the CLI Manual uploaded by the [`linux`](#linux) job. This is not pushed until the package repository artifacts are set up later. -The `cli.github.com` repository is checked out using a short-lived GitHub App installation token rather than a long-lived PAT. The `Generate site deploy token` step (which only runs when `inputs.environment == 'production'`) uses [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) with the `SITE_DEPLOY_APP_CLIENT_ID` / `SITE_DEPLOY_APP_PRIVATE_KEY` secrets to mint a token scoped to the `github/cli.github.com` repository, replacing the previous `SITE_DEPLOY_PAT` secret. In non-production environments the token step is skipped and the checkout falls back to `github.token` for anonymous read access (`token: ${{ steps.site-deploy-token.outputs.token || github.token }}`); pushing to the site is gated separately (see [Publishing behaviour and dry runs](#dry-run)). +The `cli.github.com` repository is checked out using a short-lived GitHub App installation token rather than a long-lived PAT. The `Generate site deploy token` step (which only runs when `inputs.environment == 'production'`) uses [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) with the `SITE_DEPLOY_APP_CLIENT_ID` / `SITE_DEPLOY_APP_PRIVATE_KEY` secrets to mint a token scoped to the `github/cli.github.com` repository, replacing the previous `SITE_DEPLOY_PAT` secret. The site-related steps (`Checkout documentation site`, `Update site man pages`, `Run createrepo`, `Run reprepro`, and `Publish site`) are all guarded by `if: inputs.environment == 'production'`, so in non-production environments the site is neither checked out nor mutated. Even in production, pushing to the site is gated separately on `DO_PUBLISH` (see [Publishing behaviour and dry runs](#dry-run)). ### Site Package Repositories @@ -719,6 +716,9 @@ The `Create the release` and `Publish site` steps consult their `DO_PUBLISH` env To make dry runs easy to spot in the Actions UI, the workflow's `run-name` appends a `(dry run)` suffix when `inputs.dry_run` is `true` (`run-name: ${{ inputs.tag_name }} / ${{ inputs.environment }}${{ inputs.dry_run == true && ' (dry run)' || '' }}`). +> [!IMPORTANT] +> The default value of `dry_run` differs depending on how the workflow is triggered. On the `workflow_dispatch` form it defaults to `true`, so a manually triggered run is a dry run unless you explicitly untick the box. `./script/release` takes the opposite default: it defaults `dry_run` to `false` and only forwards `dry_run=true` when invoked with the `--dry-run` flag (`script/release [--staging] [--dry-run] ...`). In other words, `./script/release ` performs a real release, while `./script/release --dry-run ` exercises the full pipeline without publishing. + ## Deepest Dive ### How script/release works