From 96f21272035b1eaeaf63f489d4a69a36fec85781 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Sat, 10 Oct 2026 18:15:35 +0900 Subject: [PATCH 1/7] docs: add GitHub App setup guide for release automation --- docs/release-github-app.md | 110 +++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/release-github-app.md diff --git a/docs/release-github-app.md b/docs/release-github-app.md new file mode 100644 index 0000000..42c5808 --- /dev/null +++ b/docs/release-github-app.md @@ -0,0 +1,110 @@ +# GitHub App for Release Automation + +This guide sets up the GitHub App that release workflows (such as `release-please`) use to open release pull requests, push version bumps, and create tags and GitHub releases. + +Workflows never store a long-lived release token. They store the App's client ID and private key as organization secrets, and mint a short-lived token in the step `release-github-token` at run time. + +## Why a GitHub App + +| Option | Problem | +|---|---| +| `GITHUB_TOKEN` | Events created with it do not trigger other workflows. CI does not run on the release pull request, and tags or releases it creates do not trigger `push: tags` or `release` workflows. | +| Personal access token | Tied to a person, long-lived, and usually broader than needed. Releases stop when that person leaves or the token expires. | +| **GitHub App** | Owned by the organization, limited to the permissions below, installed only on selected repositories, and its installation tokens expire after one hour. | + +## 1. Create the App + +Go to **Organization settings → Developer settings → GitHub Apps → New GitHub App** and fill in: + +| Field | Value | +|---|---| +| GitHub App name | `tedilabs-release` (any unique name) | +| Homepage URL | `https://github.com/tedilabs` | +| Webhook → Active | **Unchecked**. Release workflows call the API; the App does not need events. | +| Where can this GitHub App be installed? | **Only on this account** | + +### Permissions + +Grant only these **repository permissions**. Leave every organization and account permission at **No access**, and subscribe to no events. + +| Repository permission | Access | Used for | +|---|---|---| +| Contents | Read and write | Push the release branch (the `VERSION` bump), create tags and GitHub releases | +| Pull requests | Read and write | Open and update the release pull request | +| Issues | Read and write | Create and apply the `autorelease: pending` / `autorelease: tagged` labels (labels use the Issues API) | +| Metadata | Read-only | Required for every App; selected automatically | + +`release-please` documents the same three write permissions (`contents`, `issues`, `pull-requests`) for its workflow token. + +Do not grant **Workflows**. A release pull request only changes `VERSION` (and a changelog, if enabled). Add it only if a release ever needs to modify files under `.github/workflows/`. + +## 2. Generate credentials + +On the App's settings page: + +1. Copy the **Client ID** (it starts with `Iv`). `actions/create-github-app-token` v3 takes `client-id`; the numeric App ID input `app-id` is deprecated. +2. Under **Private keys**, select **Generate a private key**. A `.pem` file is downloaded. Store it as a secret in step 4, then delete the local file. + +## 3. Install the App + +On the App's settings page, select **Install App → tedilabs → Only select repositories** and choose the repositories that release with it. Start with the pilot repository (`terraform-aws-account`) and add more as the rollout proceeds. + +## 4. Store the credentials as organization secrets + +Use **Selected repositories** visibility and keep the list in sync with the App installation. + +| Secret | Value | +|---|---| +| `RELEASE_GITHUB_APP_CLIENT_ID` | The Client ID from step 2 | +| `RELEASE_GITHUB_APP_PRIVATE_KEY` | The full contents of the `.pem` file from step 2 | + +With the GitHub CLI (requires an organization admin): + +```sh +gh secret set RELEASE_GITHUB_APP_CLIENT_ID --org tedilabs --visibility selected --repos terraform-aws-account +gh secret set RELEASE_GITHUB_APP_PRIVATE_KEY --org tedilabs --visibility selected --repos terraform-aws-account < tedilabs-release.private-key.pem +``` + +To add a repository later, add it to the App installation **and** to the visibility of both secrets. + +## 5. Use it in a workflow + +```yaml +permissions: {} + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-latest + + steps: + - name: Generate GitHub App Token + id: release-github-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.RELEASE_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} + # Narrow the token to what this job needs, even if the App has more. + permission-contents: write + permission-issues: write + permission-pull-requests: write + + - name: Release Please + uses: googleapis/release-please-action@v5 + with: + token: ${{ steps.release-github-token.outputs.token }} +``` + +The token is scoped to the current repository by default and is revoked when the job ends. The workflow-level `permissions: {}` drops the unused `GITHUB_TOKEN` permissions. + +## What changes for maintainers + +- Release pull requests and releases are authored by `tedilabs-release[bot]` (the App name followed by `[bot]`). +- CI runs on release pull requests, and tags or releases created by the App trigger `push: tags` and `release` workflows. +- When `main` is protected by a ruleset later, add the App as a bypass actor only if the release flow needs to push to `main` directly. `release-please` itself goes through a pull request. + +## Rotating or revoking + +- **Rotate the key**: generate a new private key, update `RELEASE_GITHUB_APP_PRIVATE_KEY`, then delete the old key on the App's settings page. +- **Stop releases from a repository**: remove it from the App installation and from the secret visibility. +- **Suspected leak**: delete the private key on the App's settings page. Every token minted from it stops working within an hour, and new tokens cannot be minted. From bdd643095bd5043233720c6ea78be8eff86494d4 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Sat, 10 Oct 2026 18:16:42 +0900 Subject: [PATCH 2/7] docs: use app-id until actionlint supports client-id --- docs/release-github-app.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/release-github-app.md b/docs/release-github-app.md index 42c5808..6d60508 100644 --- a/docs/release-github-app.md +++ b/docs/release-github-app.md @@ -2,7 +2,7 @@ This guide sets up the GitHub App that release workflows (such as `release-please`) use to open release pull requests, push version bumps, and create tags and GitHub releases. -Workflows never store a long-lived release token. They store the App's client ID and private key as organization secrets, and mint a short-lived token in the step `release-github-token` at run time. +Workflows never store a long-lived release token. They store the App's ID and private key as organization secrets, and mint a short-lived token in the step `release-github-token` at run time. ## Why a GitHub App @@ -42,7 +42,7 @@ Do not grant **Workflows**. A release pull request only changes `VERSION` (and a On the App's settings page: -1. Copy the **Client ID** (it starts with `Iv`). `actions/create-github-app-token` v3 takes `client-id`; the numeric App ID input `app-id` is deprecated. +1. Copy the **App ID** (a number). `actions/create-github-app-token` v3 prefers `client-id` and marks `app-id` as deprecated, but actionlint (v1.7.12) still requires `app-id` for this action, so our workflows use `app-id` until actionlint catches up. Switch to the **Client ID** (it starts with `Iv`) and `client-id` then. 2. Under **Private keys**, select **Generate a private key**. A `.pem` file is downloaded. Store it as a secret in step 4, then delete the local file. ## 3. Install the App @@ -55,13 +55,13 @@ Use **Selected repositories** visibility and keep the list in sync with the App | Secret | Value | |---|---| -| `RELEASE_GITHUB_APP_CLIENT_ID` | The Client ID from step 2 | +| `RELEASE_GITHUB_APP_ID` | The App ID from step 2 | | `RELEASE_GITHUB_APP_PRIVATE_KEY` | The full contents of the `.pem` file from step 2 | With the GitHub CLI (requires an organization admin): ```sh -gh secret set RELEASE_GITHUB_APP_CLIENT_ID --org tedilabs --visibility selected --repos terraform-aws-account +gh secret set RELEASE_GITHUB_APP_ID --org tedilabs --visibility selected --repos terraform-aws-account gh secret set RELEASE_GITHUB_APP_PRIVATE_KEY --org tedilabs --visibility selected --repos terraform-aws-account < tedilabs-release.private-key.pem ``` @@ -82,7 +82,8 @@ jobs: id: release-github-token uses: actions/create-github-app-token@v3 with: - client-id: ${{ secrets.RELEASE_GITHUB_APP_CLIENT_ID }} + # `client-id` replaces `app-id` in v3, but actionlint does not know it yet. + app-id: ${{ secrets.RELEASE_GITHUB_APP_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} # Narrow the token to what this job needs, even if the App has more. permission-contents: write From d2bd2dbdd3c5b463be9047845a9da0d33a5e05b9 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Sun, 11 Oct 2026 21:57:17 +0900 Subject: [PATCH 3/7] docs: add a script to set the release App secrets per repository --- docs/release-github-app.md | 14 +++++ scripts/set-release-github-app-secrets.sh | 74 +++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100755 scripts/set-release-github-app-secrets.sh diff --git a/docs/release-github-app.md b/docs/release-github-app.md index 6d60508..913c843 100644 --- a/docs/release-github-app.md +++ b/docs/release-github-app.md @@ -67,6 +67,20 @@ gh secret set RELEASE_GITHUB_APP_PRIVATE_KEY --org tedilabs --visibility selecte To add a repository later, add it to the App installation **and** to the visibility of both secrets. +### On the GitHub Free plan + +Private repositories cannot use organization secrets on the GitHub Free plan. Set the same two secrets as repository secrets in every Terraform module repository instead: + +```sh +export RELEASE_GITHUB_APP_ID=123456 +export RELEASE_GITHUB_APP_PRIVATE_KEY="$(cat tedilabs-release.private-key.pem)" +scripts/set-release-github-app-secrets.sh --dry-run # list the target repositories +scripts/set-release-github-app-secrets.sh # or pass repository names to limit the targets +unset RELEASE_GITHUB_APP_ID RELEASE_GITHUB_APP_PRIVATE_KEY +``` + +The script targets the non-archived `tedilabs` repositories named `terraform-*` with the `type-module` topic, passes the values to `gh` through standard input, and lists the repositories where it failed. Run it again after rotating the key or adding a repository. Workflows read repository secrets with the same `secrets.RELEASE_GITHUB_APP_ID` and `secrets.RELEASE_GITHUB_APP_PRIVATE_KEY` expressions. + ## 5. Use it in a workflow ```yaml diff --git a/scripts/set-release-github-app-secrets.sh b/scripts/set-release-github-app-secrets.sh new file mode 100755 index 0000000..f19235a --- /dev/null +++ b/scripts/set-release-github-app-secrets.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Set the release GitHub App credentials as repository secrets in every Terraform module repository. +# +# On the GitHub Free plan, private repositories cannot use organization secrets, so this sets repository +# secrets instead. The values are read from environment variables and passed to `gh` through standard input, +# so they never appear in command-line arguments. +# +# Usage: +# export RELEASE_GITHUB_APP_ID=123456 +# export RELEASE_GITHUB_APP_PRIVATE_KEY="$(cat tedilabs-release.private-key.pem)" +# scripts/set-release-github-app-secrets.sh [--dry-run] [REPOSITORY...] +# +# Without REPOSITORY arguments, it targets every non-archived repository of the owner (`GITHUB_OWNER`, defaults to +# `tedilabs`) whose name starts with `terraform-` and that has the `type-module` topic. +# Requires `gh` authenticated as a user who can manage secrets of those repositories. +set -euo pipefail + +owner="${GITHUB_OWNER:-tedilabs}" +dry_run=false +repos=() +for arg in "$@"; do + case "$arg" in + --dry-run) dry_run=true ;; + -h | --help) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -*) echo "[x] Unknown option: $arg" >&2; exit 1 ;; + *) repos+=("$arg") ;; + esac +done + +: "${RELEASE_GITHUB_APP_ID:?Set RELEASE_GITHUB_APP_ID to the numeric App ID.}" +: "${RELEASE_GITHUB_APP_PRIVATE_KEY:?Set RELEASE_GITHUB_APP_PRIVATE_KEY to the contents of the private key (.pem).}" +if [[ ! $RELEASE_GITHUB_APP_ID =~ ^[0-9]+$ ]]; then + echo "[x] RELEASE_GITHUB_APP_ID must be the numeric App ID, not the Client ID." >&2 + exit 1 +fi +if [[ $RELEASE_GITHUB_APP_PRIVATE_KEY != "-----BEGIN "*"PRIVATE KEY-----"* ]]; then + echo "[x] RELEASE_GITHUB_APP_PRIVATE_KEY must be the contents of the .pem file, not its path." >&2 + exit 1 +fi + +if [ ${#repos[@]} -eq 0 ]; then + while IFS= read -r repo; do + repos+=("$repo") + done < <(gh repo list "$owner" --topic type-module --no-archived --limit 1000 --json name \ + --jq '.[].name | select(startswith("terraform-"))' | sort) +fi +if [ ${#repos[@]} -eq 0 ]; then + echo "[x] No repositories found in '${owner}'." >&2 + exit 1 +fi +echo "[!] Setting RELEASE_GITHUB_APP_ID and RELEASE_GITHUB_APP_PRIVATE_KEY in ${#repos[@]} repositories of '${owner}'." + +failed=() +for repo in "${repos[@]}"; do + if $dry_run; then + echo " would set: ${owner}/${repo}" + continue + fi + if printf '%s' "$RELEASE_GITHUB_APP_ID" | gh secret set RELEASE_GITHUB_APP_ID --repo "${owner}/${repo}" > /dev/null \ + && printf '%s' "$RELEASE_GITHUB_APP_PRIVATE_KEY" | gh secret set RELEASE_GITHUB_APP_PRIVATE_KEY --repo "${owner}/${repo}" > /dev/null; then + echo " set: ${owner}/${repo}" + else + echo "[x] failed: ${owner}/${repo}" >&2 + failed+=("$repo") + fi +done + +if [ ${#failed[@]} -gt 0 ]; then + echo "[x] Failed in ${#failed[@]} repositories: ${failed[*]}" >&2 + exit 1 +fi +if ! $dry_run; then + echo "[!] Done. Make sure the GitHub App is installed on the same repositories." +fi From e7e54284e09f0564a545b5508ddfffeb126050ea Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 12 Oct 2026 00:11:29 +0900 Subject: [PATCH 4/7] docs: pass the App ID as `client-id` --- docs/release-github-app.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/release-github-app.md b/docs/release-github-app.md index 913c843..869166a 100644 --- a/docs/release-github-app.md +++ b/docs/release-github-app.md @@ -42,7 +42,7 @@ Do not grant **Workflows**. A release pull request only changes `VERSION` (and a On the App's settings page: -1. Copy the **App ID** (a number). `actions/create-github-app-token` v3 prefers `client-id` and marks `app-id` as deprecated, but actionlint (v1.7.12) still requires `app-id` for this action, so our workflows use `app-id` until actionlint catches up. Switch to the **Client ID** (it starts with `Iv`) and `client-id` then. +1. Copy the **App ID** (a number). Workflows pass it as the `client-id` input of `actions/create-github-app-token` v3, which replaces the deprecated `app-id` and accepts either the App ID or the **Client ID** (it starts with `Iv`). 2. Under **Private keys**, select **Generate a private key**. A `.pem` file is downloaded. Store it as a secret in step 4, then delete the local file. ## 3. Install the App @@ -96,8 +96,8 @@ jobs: id: release-github-token uses: actions/create-github-app-token@v3 with: - # `client-id` replaces `app-id` in v3, but actionlint does not know it yet. - app-id: ${{ secrets.RELEASE_GITHUB_APP_ID }} + # `client-id` replaces the deprecated `app-id` and also accepts the numeric App ID. + client-id: ${{ secrets.RELEASE_GITHUB_APP_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} # Narrow the token to what this job needs, even if the App has more. permission-contents: write From 420ea4a0a10d2443b40d14e207265026ba38a856 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 12 Oct 2026 00:42:42 +0900 Subject: [PATCH 5/7] docs: store the release App Client ID as `RELEASE_GITHUB_APP_CLIENT_ID` --- docs/release-github-app.md | 17 ++++++++--------- scripts/set-release-github-app-secrets.sh | 12 ++++++------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/release-github-app.md b/docs/release-github-app.md index 869166a..5b74bc2 100644 --- a/docs/release-github-app.md +++ b/docs/release-github-app.md @@ -2,7 +2,7 @@ This guide sets up the GitHub App that release workflows (such as `release-please`) use to open release pull requests, push version bumps, and create tags and GitHub releases. -Workflows never store a long-lived release token. They store the App's ID and private key as organization secrets, and mint a short-lived token in the step `release-github-token` at run time. +Workflows never store a long-lived release token. They store the App's Client ID and private key as organization secrets, and mint a short-lived token in the step `release-github-token` at run time. ## Why a GitHub App @@ -42,7 +42,7 @@ Do not grant **Workflows**. A release pull request only changes `VERSION` (and a On the App's settings page: -1. Copy the **App ID** (a number). Workflows pass it as the `client-id` input of `actions/create-github-app-token` v3, which replaces the deprecated `app-id` and accepts either the App ID or the **Client ID** (it starts with `Iv`). +1. Copy the **Client ID** (it starts with `Iv`), not the numeric App ID. Workflows pass it as the `client-id` input of `actions/create-github-app-token` v3, which replaces the deprecated `app-id`. 2. Under **Private keys**, select **Generate a private key**. A `.pem` file is downloaded. Store it as a secret in step 4, then delete the local file. ## 3. Install the App @@ -55,13 +55,13 @@ Use **Selected repositories** visibility and keep the list in sync with the App | Secret | Value | |---|---| -| `RELEASE_GITHUB_APP_ID` | The App ID from step 2 | +| `RELEASE_GITHUB_APP_CLIENT_ID` | The Client ID from step 2 | | `RELEASE_GITHUB_APP_PRIVATE_KEY` | The full contents of the `.pem` file from step 2 | With the GitHub CLI (requires an organization admin): ```sh -gh secret set RELEASE_GITHUB_APP_ID --org tedilabs --visibility selected --repos terraform-aws-account +gh secret set RELEASE_GITHUB_APP_CLIENT_ID --org tedilabs --visibility selected --repos terraform-aws-account gh secret set RELEASE_GITHUB_APP_PRIVATE_KEY --org tedilabs --visibility selected --repos terraform-aws-account < tedilabs-release.private-key.pem ``` @@ -72,14 +72,14 @@ To add a repository later, add it to the App installation **and** to the visibil Private repositories cannot use organization secrets on the GitHub Free plan. Set the same two secrets as repository secrets in every Terraform module repository instead: ```sh -export RELEASE_GITHUB_APP_ID=123456 +export RELEASE_GITHUB_APP_CLIENT_ID=Iv23li0123456789abcd export RELEASE_GITHUB_APP_PRIVATE_KEY="$(cat tedilabs-release.private-key.pem)" scripts/set-release-github-app-secrets.sh --dry-run # list the target repositories scripts/set-release-github-app-secrets.sh # or pass repository names to limit the targets -unset RELEASE_GITHUB_APP_ID RELEASE_GITHUB_APP_PRIVATE_KEY +unset RELEASE_GITHUB_APP_CLIENT_ID RELEASE_GITHUB_APP_PRIVATE_KEY ``` -The script targets the non-archived `tedilabs` repositories named `terraform-*` with the `type-module` topic, passes the values to `gh` through standard input, and lists the repositories where it failed. Run it again after rotating the key or adding a repository. Workflows read repository secrets with the same `secrets.RELEASE_GITHUB_APP_ID` and `secrets.RELEASE_GITHUB_APP_PRIVATE_KEY` expressions. +The script targets the non-archived `tedilabs` repositories named `terraform-*` with the `type-module` topic, passes the values to `gh` through standard input, and lists the repositories where it failed. Run it again after rotating the key or adding a repository. Workflows read repository secrets with the same `secrets.RELEASE_GITHUB_APP_CLIENT_ID` and `secrets.RELEASE_GITHUB_APP_PRIVATE_KEY` expressions. ## 5. Use it in a workflow @@ -96,8 +96,7 @@ jobs: id: release-github-token uses: actions/create-github-app-token@v3 with: - # `client-id` replaces the deprecated `app-id` and also accepts the numeric App ID. - client-id: ${{ secrets.RELEASE_GITHUB_APP_ID }} + client-id: ${{ secrets.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} # Narrow the token to what this job needs, even if the App has more. permission-contents: write diff --git a/scripts/set-release-github-app-secrets.sh b/scripts/set-release-github-app-secrets.sh index f19235a..9224163 100755 --- a/scripts/set-release-github-app-secrets.sh +++ b/scripts/set-release-github-app-secrets.sh @@ -6,7 +6,7 @@ # so they never appear in command-line arguments. # # Usage: -# export RELEASE_GITHUB_APP_ID=123456 +# export RELEASE_GITHUB_APP_CLIENT_ID=Iv23li0123456789abcd # export RELEASE_GITHUB_APP_PRIVATE_KEY="$(cat tedilabs-release.private-key.pem)" # scripts/set-release-github-app-secrets.sh [--dry-run] [REPOSITORY...] # @@ -27,10 +27,10 @@ for arg in "$@"; do esac done -: "${RELEASE_GITHUB_APP_ID:?Set RELEASE_GITHUB_APP_ID to the numeric App ID.}" +: "${RELEASE_GITHUB_APP_CLIENT_ID:?Set RELEASE_GITHUB_APP_CLIENT_ID to the Client ID of the App.}" : "${RELEASE_GITHUB_APP_PRIVATE_KEY:?Set RELEASE_GITHUB_APP_PRIVATE_KEY to the contents of the private key (.pem).}" -if [[ ! $RELEASE_GITHUB_APP_ID =~ ^[0-9]+$ ]]; then - echo "[x] RELEASE_GITHUB_APP_ID must be the numeric App ID, not the Client ID." >&2 +if [[ ! $RELEASE_GITHUB_APP_CLIENT_ID =~ ^Iv[0-9A-Za-z.]+$ ]]; then + echo "[x] RELEASE_GITHUB_APP_CLIENT_ID must be the Client ID (it starts with 'Iv'), not the numeric App ID." >&2 exit 1 fi if [[ $RELEASE_GITHUB_APP_PRIVATE_KEY != "-----BEGIN "*"PRIVATE KEY-----"* ]]; then @@ -48,7 +48,7 @@ if [ ${#repos[@]} -eq 0 ]; then echo "[x] No repositories found in '${owner}'." >&2 exit 1 fi -echo "[!] Setting RELEASE_GITHUB_APP_ID and RELEASE_GITHUB_APP_PRIVATE_KEY in ${#repos[@]} repositories of '${owner}'." +echo "[!] Setting RELEASE_GITHUB_APP_CLIENT_ID and RELEASE_GITHUB_APP_PRIVATE_KEY in ${#repos[@]} repositories of '${owner}'." failed=() for repo in "${repos[@]}"; do @@ -56,7 +56,7 @@ for repo in "${repos[@]}"; do echo " would set: ${owner}/${repo}" continue fi - if printf '%s' "$RELEASE_GITHUB_APP_ID" | gh secret set RELEASE_GITHUB_APP_ID --repo "${owner}/${repo}" > /dev/null \ + if printf '%s' "$RELEASE_GITHUB_APP_CLIENT_ID" | gh secret set RELEASE_GITHUB_APP_CLIENT_ID --repo "${owner}/${repo}" > /dev/null \ && printf '%s' "$RELEASE_GITHUB_APP_PRIVATE_KEY" | gh secret set RELEASE_GITHUB_APP_PRIVATE_KEY --repo "${owner}/${repo}" > /dev/null; then echo " set: ${owner}/${repo}" else From bd0e1816cfba79727d38e71ebbe9cdf297c72170 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 12 Oct 2026 00:51:23 +0900 Subject: [PATCH 6/7] docs: delete the legacy `RELEASE_GITHUB_APP_ID` secret in the secrets script --- docs/release-github-app.md | 2 +- scripts/set-release-github-app-secrets.sh | 32 +++++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/docs/release-github-app.md b/docs/release-github-app.md index 5b74bc2..663d92f 100644 --- a/docs/release-github-app.md +++ b/docs/release-github-app.md @@ -79,7 +79,7 @@ scripts/set-release-github-app-secrets.sh # or pass repository names unset RELEASE_GITHUB_APP_CLIENT_ID RELEASE_GITHUB_APP_PRIVATE_KEY ``` -The script targets the non-archived `tedilabs` repositories named `terraform-*` with the `type-module` topic, passes the values to `gh` through standard input, and lists the repositories where it failed. Run it again after rotating the key or adding a repository. Workflows read repository secrets with the same `secrets.RELEASE_GITHUB_APP_CLIENT_ID` and `secrets.RELEASE_GITHUB_APP_PRIVATE_KEY` expressions. +The script targets the non-archived `tedilabs` repositories named `terraform-*` with the `type-module` topic, passes the values to `gh` through standard input, deletes the `RELEASE_GITHUB_APP_ID` secret that an earlier version of this guide used, and lists the repositories where it failed. Run it again after rotating the key or adding a repository. Workflows read repository secrets with the same `secrets.RELEASE_GITHUB_APP_CLIENT_ID` and `secrets.RELEASE_GITHUB_APP_PRIVATE_KEY` expressions. ## 5. Use it in a workflow diff --git a/scripts/set-release-github-app-secrets.sh b/scripts/set-release-github-app-secrets.sh index 9224163..8209893 100755 --- a/scripts/set-release-github-app-secrets.sh +++ b/scripts/set-release-github-app-secrets.sh @@ -3,7 +3,8 @@ # # On the GitHub Free plan, private repositories cannot use organization secrets, so this sets repository # secrets instead. The values are read from environment variables and passed to `gh` through standard input, -# so they never appear in command-line arguments. +# so they never appear in command-line arguments. Once both are set, the `RELEASE_GITHUB_APP_ID` secret that an +# earlier version of this script set is deleted from the repository. # # Usage: # export RELEASE_GITHUB_APP_CLIENT_ID=Iv23li0123456789abcd @@ -16,12 +17,13 @@ set -euo pipefail owner="${GITHUB_OWNER:-tedilabs}" +legacy_secret=RELEASE_GITHUB_APP_ID dry_run=false repos=() for arg in "$@"; do case "$arg" in --dry-run) dry_run=true ;; - -h | --help) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h | --help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; -*) echo "[x] Unknown option: $arg" >&2; exit 1 ;; *) repos+=("$arg") ;; esac @@ -52,13 +54,33 @@ echo "[!] Setting RELEASE_GITHUB_APP_CLIENT_ID and RELEASE_GITHUB_APP_PRIVATE_KE failed=() for repo in "${repos[@]}"; do + # Only an existing legacy secret is deleted, so a failed delete is a real failure. + if ! names=$(gh secret list --repo "${owner}/${repo}" --json name --jq '.[].name'); then + echo "[x] failed to list secrets: ${owner}/${repo}" >&2 + failed+=("$repo") + continue + fi + delete_legacy=false + if grep -qx "$legacy_secret" <<< "$names"; then + delete_legacy=true + fi + if $dry_run; then - echo " would set: ${owner}/${repo}" + if $delete_legacy; then + echo " would set: ${owner}/${repo}, would delete: ${legacy_secret}" + else + echo " would set: ${owner}/${repo}" + fi continue fi if printf '%s' "$RELEASE_GITHUB_APP_CLIENT_ID" | gh secret set RELEASE_GITHUB_APP_CLIENT_ID --repo "${owner}/${repo}" > /dev/null \ - && printf '%s' "$RELEASE_GITHUB_APP_PRIVATE_KEY" | gh secret set RELEASE_GITHUB_APP_PRIVATE_KEY --repo "${owner}/${repo}" > /dev/null; then - echo " set: ${owner}/${repo}" + && printf '%s' "$RELEASE_GITHUB_APP_PRIVATE_KEY" | gh secret set RELEASE_GITHUB_APP_PRIVATE_KEY --repo "${owner}/${repo}" > /dev/null \ + && { ! $delete_legacy || gh secret delete "$legacy_secret" --repo "${owner}/${repo}" > /dev/null; }; then + if $delete_legacy; then + echo " set: ${owner}/${repo}, deleted: ${legacy_secret}" + else + echo " set: ${owner}/${repo}" + fi else echo "[x] failed: ${owner}/${repo}" >&2 failed+=("$repo") From e0004a192e15e416bf6f922188ea209fd4e0cdae Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 12 Oct 2026 01:42:27 +0900 Subject: [PATCH 7/7] docs: use the `github.release-please` action in the workflow example --- docs/release-github-app.md | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/docs/release-github-app.md b/docs/release-github-app.md index 663d92f..d8f88ca 100644 --- a/docs/release-github-app.md +++ b/docs/release-github-app.md @@ -2,7 +2,7 @@ This guide sets up the GitHub App that release workflows (such as `release-please`) use to open release pull requests, push version bumps, and create tags and GitHub releases. -Workflows never store a long-lived release token. They store the App's Client ID and private key as organization secrets, and mint a short-lived token in the step `release-github-token` at run time. +Workflows never store a long-lived release token. They store the App's Client ID and private key as organization secrets, and mint a short-lived token at run time with the `github.release-please` action. ## Why a GitHub App @@ -42,7 +42,7 @@ Do not grant **Workflows**. A release pull request only changes `VERSION` (and a On the App's settings page: -1. Copy the **Client ID** (it starts with `Iv`), not the numeric App ID. Workflows pass it as the `client-id` input of `actions/create-github-app-token` v3, which replaces the deprecated `app-id`. +1. Copy the **Client ID** (it starts with `Iv`), not the numeric App ID. Workflows pass it to the `github_app_client_id` input of `github.release-please`, which passes it as the `client-id` input of `actions/create-github-app-token` v3. `client-id` replaces the deprecated `app-id`. 2. Under **Private keys**, select **Generate a private key**. A `.pem` file is downloaded. Store it as a secret in step 4, then delete the local file. ## 3. Install the App @@ -92,24 +92,14 @@ jobs: runs-on: ubuntu-latest steps: - - name: Generate GitHub App Token - id: release-github-token - uses: actions/create-github-app-token@v3 - with: - client-id: ${{ secrets.RELEASE_GITHUB_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} - # Narrow the token to what this job needs, even if the App has more. - permission-contents: write - permission-issues: write - permission-pull-requests: write - - name: Release Please - uses: googleapis/release-please-action@v5 + uses: tedilabs/github-actions/.github/actions/github.release-please@main with: - token: ${{ steps.release-github-token.outputs.token }} + github_app_client_id: ${{ secrets.RELEASE_GITHUB_APP_CLIENT_ID }} + github_app_private_key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} ``` -The token is scoped to the current repository by default and is revoked when the job ends. The workflow-level `permissions: {}` drops the unused `GITHUB_TOKEN` permissions. +The action mints a token narrowed to `contents`, `issues`, and `pull-requests` write, even if the App has more permissions, and runs `release-please` with `release-please-config.json` and `.release-please-manifest.json`. The token is scoped to the current repository and is revoked when the job ends. The workflow-level `permissions: {}` drops the unused `GITHUB_TOKEN` permissions. ## What changes for maintainers