diff --git a/.github/workflows/shared/squad.md b/.github/workflows/shared/squad.md index 44a12a385f4..1efa9b8cda6 100644 --- a/.github/workflows/shared/squad.md +++ b/.github/workflows/shared/squad.md @@ -13,6 +13,8 @@ # organizations or private repositories beyond the current one): # vars.SQUAD_GITHUB_APP_ID / secrets.SQUAD_GITHUB_APP_PRIVATE_KEY / vars.SQUAD_GITHUB_APP_OWNER # — mints a GitHub App installation token for `squad init` +# vars.SQUAD_GITHUB_APP_REPOSITORIES +# — comma/newline-separated repositories to scope the token to when using another owner # secrets.SQUAD_GITHUB_TOKEN # — used if the App id is not set # Auth precedence: GitHub App installation token > SQUAD_GITHUB_TOKEN > the workflow's @@ -49,6 +51,8 @@ jobs: app-id: ${{ vars.SQUAD_GITHUB_APP_ID }} private-key: ${{ secrets.SQUAD_GITHUB_APP_PRIVATE_KEY }} owner: ${{ vars.SQUAD_GITHUB_APP_OWNER }} + repositories: ${{ vars.SQUAD_GITHUB_APP_REPOSITORIES || github.repository }} + permission-contents: read - name: Initialize Squad team if: ${{ steps.squad-installation.outputs.installed != 'true' }} env: diff --git a/.github/workflows/squad-game-planner.lock.yml b/.github/workflows/squad-game-planner.lock.yml index 0295f470482..32279a21afd 100644 --- a/.github/workflows/squad-game-planner.lock.yml +++ b/.github/workflows/squad-game-planner.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"73e9019aace19f5da2bd1fb4334518a454d0582b058ad9412153584d169be609","body_hash":"d65912944855bafc98db186548a08fe776c51742722f878d8318e3be210d6dfc","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.90"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"711357dc9263d1a692b981d65de80d4fb1fa1ba9e597ad2245116a3d7d4d6df7","body_hash":"d65912944855bafc98db186548a08fe776c51742722f878d8318e3be210d6dfc","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.90"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","SQUAD_GITHUB_APP_PRIVATE_KEY","SQUAD_GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/create-github-app-token","sha":"bcd2ba49218906704ab6c1aa796996da409d3eb1","version":"v3.2.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.31","digest":"sha256:c4ab1d48d533cc7daaa5f2e1193d1a644888242fb8813d49c6de46d129224b76","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.31@sha256:c4ab1d48d533cc7daaa5f2e1193d1a644888242fb8813d49c6de46d129224b76"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.31","digest":"sha256:a5a37489635109334a5e2b5cb2eaba32e8c8f1a89d8dea6009baa962a8904c4a","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.31@sha256:a5a37489635109334a5e2b5cb2eaba32e8c8f1a89d8dea6009baa962a8904c4a"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.31","digest":"sha256:505df66052e6688cf419ad248a511d17093475bd66a958618078d3c9cdf95083","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.31@sha256:505df66052e6688cf419ad248a511d17093475bd66a958618078d3c9cdf95083"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.31","digest":"sha256:90a46d2e6e910ace2c09c2dcf6b56dc374ed513d9da642a4e9fcfcd6e9248a9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.31@sha256:90a46d2e6e910ace2c09c2dcf6b56dc374ed513d9da642a4e9fcfcd6e9248a9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.28","digest":"sha256:69920365d9f0143ed853743c319b2bd4614ca7f9537474ad8eff0c47da84c5a4","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.28@sha256:69920365d9f0143ed853743c319b2bd4614ca7f9537474ad8eff0c47da84c5a4"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"},{"image":"ghcr.io/github/github-mcp-server:v1.12.2","digest":"sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6","pinned_image":"ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop"]}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -370,7 +370,9 @@ jobs: with: app-id: ${{ vars.SQUAD_GITHUB_APP_ID }} owner: ${{ vars.SQUAD_GITHUB_APP_OWNER }} + permission-contents: read private-key: ${{ secrets.SQUAD_GITHUB_APP_PRIVATE_KEY }} + repositories: ${{ vars.SQUAD_GITHUB_APP_REPOSITORIES || github.repository }} - name: Initialize Squad team if: ${{ steps.squad-installation.outputs.installed != 'true' }} run: npx --yes "@bradygaster/squad-cli@${SQUAD_CLI_VERSION:-0.11.0}" init --preset default diff --git a/.github/workflows/squad-implement-worker.lock.yml b/.github/workflows/squad-implement-worker.lock.yml index 2b4b49a7432..5f039d6d46f 100644 --- a/.github/workflows/squad-implement-worker.lock.yml +++ b/.github/workflows/squad-implement-worker.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ba84ba582d6332325dcabc4e68f5a4a7553967b9693bc43f01f32f523dd2da46","body_hash":"93cd7801d780f9f91aa8472b85b5ae27a195211ffb1be826958e705f04009ec6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.90"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3cdc428e16f5a92603b4d6bdc1610a2f22d7a8a20c6b286b3e3067563c37a77d","body_hash":"93cd7801d780f9f91aa8472b85b5ae27a195211ffb1be826958e705f04009ec6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.90"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","SQUAD_GITHUB_APP_PRIVATE_KEY","SQUAD_GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/create-github-app-token","sha":"bcd2ba49218906704ab6c1aa796996da409d3eb1","version":"v3.2.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.31","digest":"sha256:c4ab1d48d533cc7daaa5f2e1193d1a644888242fb8813d49c6de46d129224b76","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.31@sha256:c4ab1d48d533cc7daaa5f2e1193d1a644888242fb8813d49c6de46d129224b76"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.31","digest":"sha256:a5a37489635109334a5e2b5cb2eaba32e8c8f1a89d8dea6009baa962a8904c4a","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.31@sha256:a5a37489635109334a5e2b5cb2eaba32e8c8f1a89d8dea6009baa962a8904c4a"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.31","digest":"sha256:505df66052e6688cf419ad248a511d17093475bd66a958618078d3c9cdf95083","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.31@sha256:505df66052e6688cf419ad248a511d17093475bd66a958618078d3c9cdf95083"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.31","digest":"sha256:90a46d2e6e910ace2c09c2dcf6b56dc374ed513d9da642a4e9fcfcd6e9248a9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.31@sha256:90a46d2e6e910ace2c09c2dcf6b56dc374ed513d9da642a4e9fcfcd6e9248a9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.28","digest":"sha256:69920365d9f0143ed853743c319b2bd4614ca7f9537474ad8eff0c47da84c5a4","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.28@sha256:69920365d9f0143ed853743c319b2bd4614ca7f9537474ad8eff0c47da84c5a4"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"},{"image":"ghcr.io/github/github-mcp-server:v1.12.2","digest":"sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6","pinned_image":"ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"}],"has_pull_request":true,"mcp_servers":[{"name":"safeoutputs","tools":["add_comment","create_pull_request","dispatch_workflow","missing_data","missing_tool","noop","squad"]}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -411,7 +411,9 @@ jobs: with: app-id: ${{ vars.SQUAD_GITHUB_APP_ID }} owner: ${{ vars.SQUAD_GITHUB_APP_OWNER }} + permission-contents: read private-key: ${{ secrets.SQUAD_GITHUB_APP_PRIVATE_KEY }} + repositories: ${{ vars.SQUAD_GITHUB_APP_REPOSITORIES || github.repository }} - name: Initialize Squad team if: ${{ steps.squad-installation.outputs.installed != 'true' }} run: npx --yes "@bradygaster/squad-cli@${SQUAD_CLI_VERSION:-0.11.0}" init --preset default diff --git a/.github/workflows/squad-plan.lock.yml b/.github/workflows/squad-plan.lock.yml index 4615c7bd99e..48f3bb41f45 100644 --- a/.github/workflows/squad-plan.lock.yml +++ b/.github/workflows/squad-plan.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4efc7d9a2181ef2c7ca609b0553e8577b3100cae907f06719f4460a302fab457","body_hash":"d318008725312afdde363ee66478fed51fe1999fa1dd1e63da9a4e868542d7a8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.90"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"869af60512a3111bc1deb2785a1965187448ebac591d4b44a2d1b97ddc31fdab","body_hash":"d318008725312afdde363ee66478fed51fe1999fa1dd1e63da9a4e868542d7a8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.90"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","SQUAD_GITHUB_APP_PRIVATE_KEY","SQUAD_GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/create-github-app-token","sha":"bcd2ba49218906704ab6c1aa796996da409d3eb1","version":"v3.2.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.31","digest":"sha256:c4ab1d48d533cc7daaa5f2e1193d1a644888242fb8813d49c6de46d129224b76","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.31@sha256:c4ab1d48d533cc7daaa5f2e1193d1a644888242fb8813d49c6de46d129224b76"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.31","digest":"sha256:a5a37489635109334a5e2b5cb2eaba32e8c8f1a89d8dea6009baa962a8904c4a","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.31@sha256:a5a37489635109334a5e2b5cb2eaba32e8c8f1a89d8dea6009baa962a8904c4a"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.31","digest":"sha256:505df66052e6688cf419ad248a511d17093475bd66a958618078d3c9cdf95083","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.31@sha256:505df66052e6688cf419ad248a511d17093475bd66a958618078d3c9cdf95083"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.31","digest":"sha256:90a46d2e6e910ace2c09c2dcf6b56dc374ed513d9da642a4e9fcfcd6e9248a9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.31@sha256:90a46d2e6e910ace2c09c2dcf6b56dc374ed513d9da642a4e9fcfcd6e9248a9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.28","digest":"sha256:69920365d9f0143ed853743c319b2bd4614ca7f9537474ad8eff0c47da84c5a4","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.28@sha256:69920365d9f0143ed853743c319b2bd4614ca7f9537474ad8eff0c47da84c5a4"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"},{"image":"ghcr.io/github/github-mcp-server:v1.12.2","digest":"sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6","pinned_image":"ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop"]}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -433,7 +433,9 @@ jobs: with: app-id: ${{ vars.SQUAD_GITHUB_APP_ID }} owner: ${{ vars.SQUAD_GITHUB_APP_OWNER }} + permission-contents: read private-key: ${{ secrets.SQUAD_GITHUB_APP_PRIVATE_KEY }} + repositories: ${{ vars.SQUAD_GITHUB_APP_REPOSITORIES || github.repository }} - name: Initialize Squad team if: ${{ steps.squad-installation.outputs.installed != 'true' }} run: npx --yes "@bradygaster/squad-cli@${SQUAD_CLI_VERSION:-0.11.0}" init --preset default diff --git a/.github/workflows/squad.lock.yml b/.github/workflows/squad.lock.yml index c1b92152bb2..19bb494c2c9 100644 --- a/.github/workflows/squad.lock.yml +++ b/.github/workflows/squad.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f3bb997a48c3ea82a6b25602cad4e77489da634e77418c33274c78680ea6444e","body_hash":"1c8c50093cc78ce0fce274ef4bffa201b54bb8f466dbd721975eade857a6a411","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.90"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"aba5b656ba348ce73a349f03c6ec775e44a741bd57cecc5caf132d4e44e2bbec","body_hash":"1c8c50093cc78ce0fce274ef4bffa201b54bb8f466dbd721975eade857a6a411","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.90"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","SQUAD_GITHUB_APP_PRIVATE_KEY","SQUAD_GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/create-github-app-token","sha":"bcd2ba49218906704ab6c1aa796996da409d3eb1","version":"v3.2.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.31","digest":"sha256:c4ab1d48d533cc7daaa5f2e1193d1a644888242fb8813d49c6de46d129224b76","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.31@sha256:c4ab1d48d533cc7daaa5f2e1193d1a644888242fb8813d49c6de46d129224b76"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.31","digest":"sha256:a5a37489635109334a5e2b5cb2eaba32e8c8f1a89d8dea6009baa962a8904c4a","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.31@sha256:a5a37489635109334a5e2b5cb2eaba32e8c8f1a89d8dea6009baa962a8904c4a"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.31","digest":"sha256:505df66052e6688cf419ad248a511d17093475bd66a958618078d3c9cdf95083","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.31@sha256:505df66052e6688cf419ad248a511d17093475bd66a958618078d3c9cdf95083"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.31","digest":"sha256:90a46d2e6e910ace2c09c2dcf6b56dc374ed513d9da642a4e9fcfcd6e9248a9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.31@sha256:90a46d2e6e910ace2c09c2dcf6b56dc374ed513d9da642a4e9fcfcd6e9248a9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.28","digest":"sha256:69920365d9f0143ed853743c319b2bd4614ca7f9537474ad8eff0c47da84c5a4","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.28@sha256:69920365d9f0143ed853743c319b2bd4614ca7f9537474ad8eff0c47da84c5a4"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"},{"image":"ghcr.io/github/github-mcp-server:v1.12.2","digest":"sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6","pinned_image":"ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"}],"mcp_servers":[{"name":"safeoutputs","tools":["add_comment","create_issue","create_pull_request","dispatch_workflow","missing_data","missing_tool","noop","squad_implement_worker"]}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -461,7 +461,9 @@ jobs: with: app-id: ${{ vars.SQUAD_GITHUB_APP_ID }} owner: ${{ vars.SQUAD_GITHUB_APP_OWNER }} + permission-contents: read private-key: ${{ secrets.SQUAD_GITHUB_APP_PRIVATE_KEY }} + repositories: ${{ vars.SQUAD_GITHUB_APP_REPOSITORIES || github.repository }} - name: Initialize Squad team if: ${{ steps.squad-installation.outputs.installed != 'true' }} run: npx --yes "@bradygaster/squad-cli@${SQUAD_CLI_VERSION:-0.11.0}" init --preset default diff --git a/docs/adr/64976-require-explicit-github-app-token-scopes.md b/docs/adr/64976-require-explicit-github-app-token-scopes.md new file mode 100644 index 00000000000..3dced7a72c2 --- /dev/null +++ b/docs/adr/64976-require-explicit-github-app-token-scopes.md @@ -0,0 +1,45 @@ +# ADR-64976: Require Explicit GitHub App Token Scopes + +**Date**: 2026-10-02 +**Status**: Draft +**Deciders**: gh-aw maintainers (review pending) + +--- + +### Context + +Several gh-aw workflows mint GitHub App installation tokens via `actions/create-github-app-token`. Without explicit `permission-*` inputs, those tokens inherit the GitHub App installation's full scope even when a job only needs read-only repository access. This pull request adds compile-time validation, updates the shared Squad bootstrap workflow, regenerates affected lock files, and documents the requirement so the compiler can enforce least-privilege token creation across authored, imported, and generated workflow steps. + +### Decision + +We will require every compiled `actions/create-github-app-token` step to declare at least one explicit `permission-*` input, and we will fail compilation in strict mode when those inputs are missing. In non-strict mode, the compiler will emit a warning instead of silently allowing a fully scoped installation token. We will also scope the shared Squad bootstrap token to `permission-contents: read` and document that job-level `permissions:` does not constrain GitHub App installation tokens. + +### Alternatives Considered + +#### Alternative 1: Continue Relying on Job-Level `permissions:` and Reviewer Discipline + +This keeps the compiler unchanged and relies on authors and reviewers to remember that `actions/create-github-app-token` is independent of the workflow job's `permissions:` block. It was considered because it avoids new validation logic and keeps authored workflows more flexible. It was not chosen because the PR evidence shows that unscoped app-token steps already existed in shared workflow code, making manual review insufficient for preventing over-privileged tokens. + +#### Alternative 2: Auto-Inject Default Permissions During Compilation + +The compiler could silently add a default scope such as `permission-contents: read` whenever an app-token step omits explicit scopes. It was considered because it would remediate many cases without blocking authors. It was not chosen because the correct scope depends on each workflow's actual needs, and automatic defaults would hide the architectural decision rather than forcing workflow authors to declare the minimum required permissions explicitly. + +### Consequences + +#### Positive +- GitHub App installation tokens minted by compiled workflows must declare explicit scope, reducing the chance of unintentionally granting full installation permissions. +- Strict-mode compilation catches unsafe workflow definitions before lock files are produced or merged. +- Shared Squad workflows and documentation now model the least-privilege pattern for future workflow authors. + +#### Negative +- Existing workflows that relied on implicit full-scope tokens will now fail or warn until authors update them with explicit `permission-*` inputs. +- The compiler now performs an additional validation pass over compiled YAML when app-token steps are present, adding some maintenance and test surface area. +- Authors must understand and choose the correct GitHub App permission set, which may require extra design and review effort. + +#### Neutral +- Regenerated lock files reflect the scoped token inputs and related dependency churn, but they do not change the decision itself beyond compiled output alignment. +- Non-strict workflows remain buildable with warnings, so adoption can be incremental while repositories move toward strict enforcement. + +--- + +*Draft decision record for [pull request #64976](https://github.com/github/gh-aw/pull/64976). Review before changing status to Accepted.* diff --git a/docs/src/content/docs/reference/steps-jobs.md b/docs/src/content/docs/reference/steps-jobs.md index 707e7da1399..87cc81d80ca 100644 --- a/docs/src/content/docs/reference/steps-jobs.md +++ b/docs/src/content/docs/reference/steps-jobs.md @@ -27,8 +27,15 @@ pre-steps: - name: Mint checkout token id: checkout_app uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + repositories: ${{ github.repository }} + permission-contents: read ``` +Set an explicit `repositories` input and `permission-*` inputs for every `actions/create-github-app-token` step; otherwise the installation token can inherit access across the App installation. To scope a token to the current repository, use `repositories: ${{ github.repository }}`. The action accepts the fully qualified `owner/repository` value from this trusted context. If you set `owner` to a different installation owner, explicitly list repositories belonging to that owner instead. The compiler warns when either is missing and rejects the workflow in strict mode. Job-level `permissions:` does not scope the App token. + Use pre-steps when later checkout or setup must consume outputs from a step in the same job. ```yaml wrap diff --git a/pkg/actionpins/data/action_pins.json b/pkg/actionpins/data/action_pins.json index 475807c673f..efc5e49d7ee 100644 --- a/pkg/actionpins/data/action_pins.json +++ b/pkg/actionpins/data/action_pins.json @@ -148,10 +148,10 @@ "version": "v4.38.2", "sha": "2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2" }, - "github/stale-repos@v9.0.19": { + "github/stale-repos@v9.0.18": { "repo": "github/stale-repos", - "version": "v9.0.19", - "sha": "cd662591ad9d5d8967f84431eb94a45f828c1db3" + "version": "v9.0.18", + "sha": "c42a2821e3ef0cee1b1bc20b7eecbb2bfa7a83b4" }, "haskell-actions/setup@v2.12.1": { "repo": "haskell-actions/setup", @@ -173,10 +173,10 @@ "version": "v1", "sha": "5ac0f275b83d9d5a9342c6aae977ec32fa330daa" }, - "super-linter/super-linter@v9.0.0": { + "super-linter/super-linter@v8.7.0": { "repo": "super-linter/super-linter", - "version": "v9.0.0", - "sha": "2da136927bd4a73596db63044b504547c62cb854" + "version": "v8.7.0", + "sha": "4ce20838b8ab83717e78138c5b3a1407148e0918" } }, "containers": { diff --git a/pkg/workflow/app_token_permissions_validation.go b/pkg/workflow/app_token_permissions_validation.go new file mode 100644 index 00000000000..caa6aed85ed --- /dev/null +++ b/pkg/workflow/app_token_permissions_validation.go @@ -0,0 +1,95 @@ +package workflow + +import ( + "fmt" + "os" + "strings" + + "github.com/github/gh-aw/pkg/console" +) + +func hasExplicitAppTokenPermission(with map[string]any) bool { + for key, value := range with { + if !strings.HasPrefix(strings.ToLower(key), "permission-") { + continue + } + level, ok := value.(string) + if !ok { + continue + } + switch strings.ToLower(strings.TrimSpace(level)) { + case "read", "write", "none": + return true + } + } + return false +} + +func hasExplicitAppTokenRepositories(with map[string]any) bool { + for key, value := range with { + if !strings.EqualFold(key, "repositories") { + continue + } + repositories, ok := value.(string) + return ok && strings.TrimSpace(repositories) != "" + } + return false +} + +// validateAppTokenPermissions checks the compiled jobs, including imported and +// compiler-generated steps, before an installation token can inherit every scope. +func (c *Compiler) validateAppTokenPermissions(workflow map[string]any, strict bool) error { + jobs, ok := workflow["jobs"].(map[string]any) + if !ok { + return nil + } + for jobName, jobValue := range jobs { + job, ok := jobValue.(map[string]any) + if !ok { + continue + } + steps, ok := job["steps"].([]any) + if !ok { + continue + } + for i, stepValue := range steps { + step, ok := stepValue.(map[string]any) + if !ok { + continue + } + uses, ok := step["uses"].(string) + if !ok { + continue + } + if !strings.HasPrefix(strings.ToLower(uses), "actions/create-github-app-token@") { + continue + } + with, ok := step["with"].(map[string]any) + if !ok { + with = nil + } + if !hasExplicitAppTokenRepositories(with) { + msg := fmt.Sprintf("actions/create-github-app-token in job %q has no explicit repositories input; add repositories: ${{ github.repository }} to scope the token to the current repository", jobName) + if strict { + return fmt.Errorf("strict mode: %s", msg) + } + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(msg)) + c.IncrementWarningCount() + } + if hasExplicitAppTokenPermission(with) { + continue + } + name, ok := step["name"].(string) + if !ok || name == "" { + name = fmt.Sprintf("step %d", i+1) + } + msg := fmt.Sprintf("actions/create-github-app-token in job %q (%s) has no explicit permission-* inputs; add only the required permission scopes under with: to avoid minting a fully scoped installation token", jobName, name) + if strict { + return fmt.Errorf("strict mode: %s", msg) + } + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(msg)) + c.IncrementWarningCount() + } + } + return nil +} diff --git a/pkg/workflow/app_token_permissions_validation_test.go b/pkg/workflow/app_token_permissions_validation_test.go new file mode 100644 index 00000000000..4ce9a66eeae --- /dev/null +++ b/pkg/workflow/app_token_permissions_validation_test.go @@ -0,0 +1,185 @@ +//go:build !integration + +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestAppTokenPermissionsAtCompileTime(t *testing.T) { + for _, tc := range []struct { + name string + strict string + repositories string + inputs string + uses string + wantError bool + wantWarn bool + }{ + {"strict unscoped", "", " repositories: ${{ github.repository }}\n", "", "", true, false}, + {"case variant strict unscoped", "", " repositories: ${{ github.repository }}\n", "", "Actions/create-github-app-token@v3.2.0", true, false}, + {"non-strict unscoped", "strict: false\n", " repositories: ${{ github.repository }}\n", "", "", false, true}, + {"strict empty permission", "", " repositories: ${{ github.repository }}\n", " permission-contents: ''\n", "", true, false}, + {"strict invalid permission", "", " repositories: ${{ github.repository }}\n", " permission-contents: invalid\n", "", true, false}, + {"strict none permission", "", " repositories: ${{ github.repository }}\n", " Permission-contents: none\n", "", false, false}, + {"strict scoped", "", " repositories: ${{ github.repository }}\n", " permission-contents: read\n", "", false, false}, + {"non-strict scoped", "strict: false\n", " repositories: ${{ github.repository }}\n", " permission-issues: write\n", "", false, false}, + {"strict missing repositories", "", "", " permission-contents: read\n", "", true, false}, + {"non-strict missing repositories", "strict: false\n", "", " permission-contents: read\n", "", false, true}, + {"strict empty repositories", "", " repositories: ''\n", " permission-contents: read\n", "", true, false}, + } { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "app-token.md") + uses := tc.uses + if uses == "" { + uses = "actions/create-github-app-token@v3.2.0" + } + content := "---\nname: App token test\non: workflow_dispatch\n" + tc.strict + + "network:\n allowed: [defaults]\njobs:\n activation:\n steps:\n" + + " - name: Mint app token\n uses: " + uses + "\n" + + " with:\n app-id: ${{ vars.APP_ID }}\n private-key: ${{ secrets.APP_KEY }}\n" + + tc.repositories + tc.inputs + "---\n\n# Test\n" + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + compiler := NewCompiler() + err := compiler.CompileWorkflow(path) + if tc.wantError { + if err == nil || !strings.Contains(err.Error(), "strict mode: actions/create-github-app-token") { + t.Fatalf("expected strict app token scope error, got %v", err) + } + } else if err != nil { + t.Fatalf("unexpected compile error: %v", err) + } + if tc.wantWarn && compiler.warningCount == 0 { + t.Fatal("expected a warning for unscoped app token") + } + if !tc.wantError { + if _, err := os.Stat(filepath.Join(dir, "app-token.lock.yml")); err != nil { + t.Fatalf("expected compiled lock file: %v", err) + } + } + }) + } +} + +func TestAppTokenPermissionsCheckAllCompiledJobs(t *testing.T) { + compiler := NewCompiler() + workflow := map[string]any{"jobs": map[string]any{ + "custom": map[string]any{"steps": []any{ + map[string]any{"uses": "actions/create-github-app-token@sha", "with": map[string]any{"private-key": "secret"}}, + }}, + }} + err := compiler.validateAppTokenPermissions(workflow, true) + if err == nil || !strings.Contains(err.Error(), `job "custom"`) { + t.Fatalf("expected custom job permission error, got %v", err) + } + if !strings.Contains(err.Error(), "repositories: ${{ github.repository }}") { + t.Fatalf("expected missing-repositories error to recommend the trusted repository context, got %v", err) + } +} + +func TestAppTokenPermissionsGeneratedPreActivationStep(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "app-token.md") + content := `--- +name: App token skip-if test +on: + workflow_dispatch: + skip-if-match: "is:issue is:open label:test" + github-app: + client-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_KEY }} +network: + allowed: [defaults] +--- + +# Test +` + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + compiler := NewCompiler() + if err := compiler.CompileWorkflow(path); err != nil { + t.Fatalf("expected generated pre-activation token to compile: %v", err) + } + lockContent, err := os.ReadFile(filepath.Join(dir, "app-token.lock.yml")) + if err != nil { + t.Fatalf("expected compiled lock file: %v", err) + } + for _, permission := range []string{"permission-issues: read", "permission-pull-requests: read"} { + if !strings.Contains(string(lockContent), permission) { + t.Errorf("expected generated token permissions to contain %q", permission) + } + if !strings.Contains(string(lockContent), "repositories: ${{ github.event.repository.name }}") { + t.Fatal("expected generated token to explicitly scope repositories to the current repository") + } + } +} + +func TestAppTokenPermissionsGeneratedTokenDefaults(t *testing.T) { + compiler := NewCompiler() + app := &GitHubAppConfig{AppID: "app-id", PrivateKey: "private-key"} + + steps := strings.Join(compiler.buildGitHubAppTokenMintStepWithMeta( + app, nil, "", "", "Mint token", "mint-token", + ), "") + if !strings.Contains(steps, "permission-contents: read") { + t.Fatalf("expected nil permission config to default to contents: read, got:\n%s", steps) + } + + app.Permissions = map[string]string{"contents": "none"} + steps = strings.Join(compiler.buildGitHubAppTokenMintStepWithMeta( + app, nil, "", "", "Mint token", "mint-token", + ), "") + if !strings.Contains(steps, "permission-contents: none") { + t.Fatalf("expected app permission override to be emitted with nil permissions, got:\n%s", steps) + } + + steps = strings.Join(compiler.buildGitHubAppTokenMintStepWithMeta( + app, NewPermissions(), "", "", "Mint token", "mint-token", + ), "") + if !strings.Contains(steps, "permission-contents: none") { + t.Fatalf("expected empty permissions to emit an explicit none scope, got:\n%s", steps) + } +} + +func TestAppTokenPermissionsGeneratedOTLPAndSideRepoSteps(t *testing.T) { + compiler := NewCompiler() + data := &WorkflowData{ + RawFrontmatter: map[string]any{ + "observability": map[string]any{ + "otlp": map[string]any{ + "github-app": map[string]any{ + "client-id": "app-id", + "private-key": "private-key", + }, + }, + }, + }, + } + otlpSteps := strings.Join(compiler.generateOTLPOIDCMintStep(data), "") + if !strings.Contains(otlpSteps, "permission-contents: read") { + t.Fatalf("expected OTLP token to explicitly scope repository content access, got:\n%s", otlpSteps) + } + + sideRepoSteps := sideRepoAppTokenMintStepYAML( + &GitHubAppConfig{AppID: "app-id", PrivateKey: "private-key"}, + "owner/repo", + ) + for _, permission := range []string{ + "permission-actions: read", + "permission-contents: write", + "permission-discussions: write", + "permission-issues: write", + "permission-pull-requests: write", + } { + if !strings.Contains(sideRepoSteps, permission) { + t.Errorf("expected side-repository token permission %q", permission) + } + } +} diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 24316de60b7..0aef988e636 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -192,7 +192,8 @@ func (c *Compiler) generateAndValidateYAML(workflowData *WorkflowData, markdownP // used when schema validation is disabled (skipValidation=true), where targeted // fast-path checks avoid an unnecessary yaml.Unmarshal. needsSchemaCheck := !c.skipValidation - needsWorkflowParse := needsSchemaCheck || requireSelfHostedRunners + needsAppTokenCheck := strings.Contains(strings.ToLower(yamlContent), "actions/create-github-app-token@") + needsWorkflowParse := needsSchemaCheck || requireSelfHostedRunners || needsAppTokenCheck var parsedWorkflow map[string]any if needsWorkflowParse { @@ -200,9 +201,13 @@ func (c *Compiler) generateAndValidateYAML(workflowData *WorkflowData, markdownP // parsed representation with the template injection validator below. workflowLog.Print("Parsing compiled YAML for validation") if parseErr := yaml.Unmarshal([]byte(yamlContent), &parsedWorkflow); parseErr != nil { - if requireSelfHostedRunners { + if requireSelfHostedRunners || needsAppTokenCheck { + reason := "could not inspect compiled workflow" + if requireSelfHostedRunners { + reason = "self-hosted runner policy could not inspect compiled workflow" + } return "", nil, nil, formatCompilerError(markdownPath, "error", - fmt.Sprintf("self-hosted runner policy could not inspect compiled workflow: %v", parseErr), parseErr) + fmt.Sprintf("%s: %v", reason, parseErr), parseErr) } // If parsing fails here the subsequent validators would also fail; keep going // so we surface the root error from the right validator. @@ -216,6 +221,12 @@ func (c *Compiler) generateAndValidateYAML(workflowData *WorkflowData, markdownP } } + if needsAppTokenCheck { + if err := c.validateAppTokenPermissions(parsedWorkflow, c.effectiveStrictMode(workflowData.RawFrontmatter)); err != nil { + return "", nil, nil, formatCompilerError(markdownPath, "error", err.Error(), err) + } + } + // Validate for template injection vulnerabilities (unsafe expression usage in run: commands). // // parsedWorkflow != nil means the YAML was already parsed for schema validation; diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go index 25fc87e316f..1beb3150665 100644 --- a/pkg/workflow/compiler_pre_activation_job.go +++ b/pkg/workflow/compiler_pre_activation_job.go @@ -774,45 +774,18 @@ func extractPreActivationJobOutputs(jobName string, configMap map[string]any) (m // by all skip-if checks in the pre-activation job. The step ID is "pre-activation-app-token". // Auth configuration comes from the top-level on.github-app field. func (c *Compiler) buildPreActivationAppTokenMintStep(app *GitHubAppConfig) []string { - var steps []string - tokenStepID := constants.PreActivationAppTokenStepID - - steps = append(steps, " - name: Generate GitHub App token for skip-if checks\n") - steps = append(steps, fmt.Sprintf(" id: %s\n", tokenStepID)) - if app.shouldIgnoreMissingKey() { - guard := buildIgnoreIfMissingCondition(app) - steps = appendStepEnvAssignments(steps, guard.EnvAssignments) - if guard.Condition != "" { - steps = append(steps, fmt.Sprintf(" if: %s\n", guard.Condition)) - } - } - steps = append(steps, fmt.Sprintf(" uses: %s\n", getActionPin("actions/create-github-app-token"))) - steps = append(steps, " with:\n") - steps = append(steps, fmt.Sprintf(" client-id: %s\n", app.AppID)) - steps = append(steps, fmt.Sprintf(" private-key: %s\n", app.PrivateKey)) - - owner := app.Owner - if owner == "" { - owner = "${{ github.repository_owner }}" - } - steps = append(steps, fmt.Sprintf(" owner: %s\n", owner)) - - if len(app.Repositories) == 1 && app.Repositories[0] == "*" { - // Org-wide access: omit repositories field entirely - } else if len(app.Repositories) == 1 { - steps = append(steps, fmt.Sprintf(" repositories: %s\n", app.Repositories[0])) - } else if len(app.Repositories) > 1 { - steps = append(steps, " repositories: |-\n") - for _, repo := range app.Repositories { - steps = append(steps, fmt.Sprintf(" %s\n", repo)) - } - } else { - steps = append(steps, " repositories: ${{ github.event.repository.name }}\n") - } - - steps = append(steps, " github-api-url: ${{ github.api_url }}\n") - - return steps + permissions := NewPermissionsFromMap(map[PermissionScope]PermissionLevel{ + PermissionIssues: PermissionRead, + PermissionPullRequests: PermissionRead, + }) + return c.buildGitHubAppTokenMintStepWithMeta( + app, + permissions, + "", + "", + "Generate GitHub App token for skip-if checks", + string(constants.PreActivationAppTokenStepID), + ) } // resolvePreActivationSkipIfToken returns the GitHub token expression to use for skip-if check diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index f60052c0bc9..411f2072347 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -659,7 +659,7 @@ func (c *Compiler) buildPreambleTokenSteps(data *WorkflowData, outputs map[strin headRepoSlug := getSafeOutputsHeadRepoSlug(data.SafeOutputs) preambleTokenSteps = append(preambleTokenSteps, c.buildGitHubAppTokenMintStepWithMeta( headApp, - nil, + NewPermissionsContentsWritePRWrite(), headRepoNameFromSlug(headRepoSlug), headRepoSlug, "Generate GitHub App head token", diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 475807c673f..efc5e49d7ee 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -148,10 +148,10 @@ "version": "v4.38.2", "sha": "2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2" }, - "github/stale-repos@v9.0.19": { + "github/stale-repos@v9.0.18": { "repo": "github/stale-repos", - "version": "v9.0.19", - "sha": "cd662591ad9d5d8967f84431eb94a45f828c1db3" + "version": "v9.0.18", + "sha": "c42a2821e3ef0cee1b1bc20b7eecbb2bfa7a83b4" }, "haskell-actions/setup@v2.12.1": { "repo": "haskell-actions/setup", @@ -173,10 +173,10 @@ "version": "v1", "sha": "5ac0f275b83d9d5a9342c6aae977ec32fa330daa" }, - "super-linter/super-linter@v9.0.0": { + "super-linter/super-linter@v8.7.0": { "repo": "super-linter/super-linter", - "version": "v9.0.0", - "sha": "2da136927bd4a73596db63044b504547c62cb854" + "version": "v8.7.0", + "sha": "4ce20838b8ab83717e78138c5b3a1407148e0918" } }, "containers": { diff --git a/pkg/workflow/safe_outputs_app_config.go b/pkg/workflow/safe_outputs_app_config.go index af9bad4260f..f8c1b8658b6 100644 --- a/pkg/workflow/safe_outputs_app_config.go +++ b/pkg/workflow/safe_outputs_app_config.go @@ -379,6 +379,51 @@ func (c *Compiler) buildGitHubAppTokenMintStepForRepository(app *GitHubAppConfig return c.buildGitHubAppTokenMintStepWithMeta(app, permissions, fallbackRepoExpr, ownerSourceRepository, "Generate GitHub App token", "safe-outputs-app-token") } +func appTokenPermissionFields(app *GitHubAppConfig, permissions *Permissions) map[string]string { + if permissions == nil { + permissions = NewPermissionsContentsRead() + } + permissionFields := convertPermissionsToAppTokenFields(permissions) + for key, val := range app.Permissions { + scope := convertStringToPermissionScope(key) + if scope == "" { + safeOutputsAppLog.Printf("Skipping unknown permission scope %q in github-app.permissions", key) + continue + } + level := strings.ToLower(strings.TrimSpace(val)) + tempPerms := NewPermissionsFromMap(map[PermissionScope]PermissionLevel{scope: PermissionLevel(level)}) + maps.Copy(permissionFields, convertPermissionsToAppTokenFields(tempPerms)) + } + if len(permissionFields) == 0 { + permissionFields["permission-contents"] = "none" + } + return permissionFields +} + +func appendGitHubAppTokenRepositoryInput(steps []string, app *GitHubAppConfig, fallbackRepoExpr string) []string { + if len(app.Repositories) == 1 { + for _, repository := range app.Repositories { + if repository == "*" { + safeOutputsAppLog.Print("Using org-wide GitHub App token (repositories: *)") + } else { + steps = append(steps, fmt.Sprintf(" repositories: %s\n", repository)) + } + } + } else if len(app.Repositories) > 1 { + steps = append(steps, " repositories: |-\n") + for _, repository := range app.Repositories { + steps = append(steps, fmt.Sprintf(" %s\n", repository)) + } + } else { + repoExpr := fallbackRepoExpr + if repoExpr == "" { + repoExpr = "${{ github.event.repository.name }}" + } + steps = append(steps, fmt.Sprintf(" repositories: %s\n", repoExpr)) + } + return steps +} + func (c *Compiler) buildGitHubAppTokenMintStepWithMeta(app *GitHubAppConfig, permissions *Permissions, fallbackRepoExpr string, ownerSourceRepository string, stepName string, stepID string) []string { safeOutputsAppLog.Printf("Building GitHub App token mint step: owner=%s, repos=%d", app.Owner, len(app.Repositories)) var steps []string @@ -402,69 +447,16 @@ func (c *Compiler) buildGitHubAppTokenMintStepWithMeta(app *GitHubAppConfig, per // Add owner - default to the derived checkout owner when available, otherwise current repository owner. steps = append(steps, fmt.Sprintf(" owner: %s\n", owner)) - // Add repositories - behavior depends on configuration: - // - If repositories is ["*"], omit the field to allow org-wide access - // - If repositories is a single value, use inline format - // - If repositories has multiple values, use block scalar format (newline-separated) - // to ensure clarity and proper parsing by actions/create-github-app-token - // - If repositories is empty/not specified, default to fallbackRepoExpr or the current repository - if len(app.Repositories) == 1 && app.Repositories[0] == "*" { - // Org-wide access: omit repositories field entirely - safeOutputsAppLog.Print("Using org-wide GitHub App token (repositories: *)") - } else if len(app.Repositories) == 1 { - // Single repository: use inline format for clarity - steps = append(steps, fmt.Sprintf(" repositories: %s\n", app.Repositories[0])) - } else if len(app.Repositories) > 1 { - // Multiple repositories: use block scalar format (newline-separated) - // This format is more readable and avoids potential issues with comma-separated parsing - steps = append(steps, " repositories: |-\n") - for _, repo := range app.Repositories { - steps = append(steps, fmt.Sprintf(" %s\n", repo)) - } - } else { - // No explicit repositories: use fallback expression, or default to the triggering repo's name. - // For workflow_call relay scenarios the caller passes needs.activation.outputs.target_repo_name so - // the token is scoped to the platform (host) repo name rather than the full owner/repo slug. - repoExpr := fallbackRepoExpr - if repoExpr == "" { - repoExpr = "${{ github.event.repository.name }}" - } - steps = append(steps, fmt.Sprintf(" repositories: %s\n", repoExpr)) - } + steps = appendGitHubAppTokenRepositoryInput(steps, app, fallbackRepoExpr) // Always add github-api-url from environment variable steps = append(steps, " github-api-url: ${{ github.api_url }}\n") - // Add permission-* fields automatically computed from job permissions. - // Sort keys to ensure deterministic compilation order. - if permissions != nil { - permissionFields := convertPermissionsToAppTokenFields(permissions) + permissionFields := appTokenPermissionFields(app, permissions) + keys := sliceutil.SortedKeys(permissionFields) - // Apply app.Permissions overrides on top of handler-computed permissions. - // This allows workflows to add GitHub App-only scopes (e.g. members: read, - // organization-administration: read) that are not expressible via standard - // safe-output handler declarations. The override wins over the computed value - // for any scope it declares. - for key, val := range app.Permissions { - scope := convertStringToPermissionScope(key) - if scope == "" { - safeOutputsAppLog.Printf("Skipping unknown permission scope %q in github-app.permissions", key) - continue - } - level := strings.ToLower(strings.TrimSpace(val)) - // Map the scope back to a permission-* field name by running it through - // a single-entry Permissions object so the same mapping logic applies. - tempPerms := NewPermissionsFromMap(map[PermissionScope]PermissionLevel{scope: PermissionLevel(level)}) - maps.Copy(permissionFields, convertPermissionsToAppTokenFields(tempPerms)) - } - - // Extract and sort keys for deterministic ordering - keys := sliceutil.SortedKeys(permissionFields) - - // Add permissions in sorted order - for _, key := range keys { - steps = append(steps, fmt.Sprintf(" %s: %s\n", key, permissionFields[key])) - } + for _, key := range keys { + steps = append(steps, fmt.Sprintf(" %s: %s\n", key, permissionFields[key])) } return steps diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go index d818cff1ec5..288d596fc6f 100644 --- a/pkg/workflow/side_repo_maintenance.go +++ b/pkg/workflow/side_repo_maintenance.go @@ -144,7 +144,13 @@ func sideRepoAppTokenMintStepYAML(app *GitHubAppConfig, targetRepo string) strin var c Compiler lines := c.buildGitHubAppTokenMintStepWithMeta( app, - nil, // no additional permission scoping; the app's installation grants determine access + NewPermissionsFromMap(map[PermissionScope]PermissionLevel{ + PermissionActions: PermissionRead, + PermissionContents: PermissionWrite, + PermissionDiscussions: PermissionWrite, + PermissionIssues: PermissionWrite, + PermissionPullRequests: PermissionWrite, + }), targetRepo, targetRepo, "Generate GitHub App token",