diff --git a/README.md b/README.md index e6158fa..552c20a 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ These actions supersede the [`rstudio/actions/connect-publish`](https://github.c There are a few prerequisites to set up before you can use these actions: 1. Deploy your content to Connect for the first time by other means. The `deploy` action here will not create a new content item for you; it will only update an existing one with new code. If you use the Publisher extension for Positron, VS Code, or other Code OSS forks, check in the `.posit/` TOML files it creates---this action can detect and use them. Otherwise, you just need the content's URL. -2. Configure auth. If your Connect server is version 2026.07.0 or newer and has an Enhanced or Advanced license, we recommend using the Trusted Publishing feature, which allows you to publish from this GitHub repository automatically, no API keys needed. You can enable this in the "Access" tab of the content settings. If you are not using Trusted Publishing, you will need to get an API key with at least "publisher" privileges from your Connect account and add it as a GitHub Actions secret. +2. Configure auth. If your Connect server is version 2026.07.0 or newer and has an Enhanced or Advanced license, we recommend using the Trusted Publishing feature, which allows you to publish from this GitHub repository automatically, no API keys needed. You can enable this in the "Access" tab of the content settings; see [Trusted publishing](#trusted-publishing) below for what to enter, including for repositories that use GitHub's immutable subject claims. If you are not using Trusted Publishing, you will need to get an API key with at least "publisher" privileges from your Connect account and add it as a GitHub Actions secret. 3. Make sure your requirements files are checked in. For Python content, this can either be a `uv.lock` file or a `requirements.txt`, and if you have neither, one can be generated from a `pyproject.toml` file. (We recommend that you keep both `pyproject.toml` and one of those lockfiles and use [Dependabot](https://docs.github.com/en/code-security/dependabot) to update the lockfile on a schedule so that your content stays up to date and security vulnerabilities are resolved.) For R, use the `rsconnect::writeManifest()` function to generate a `manifest.json` file. Then, you can add these actions. There are examples below, or you can let an AI agent set them up for you with the bundled Agent Skill. ## Set up with an Agent Skill -This repo includes an [Agent Skill](https://agentskills.io) (`setup-connect-deploy`) that walks a coding agent through adding these workflows to your repository: it checks your prerequisites, reads your `.posit` deployment file (or asks for the server URL and content GUID), asks whether you're using Trusted Publishing or an API key, and writes the `deploy` (and optionally `cleanup-previews`) workflow for you. +This repo includes an [Agent Skill](https://agentskills.io) (`setup-connect-deploy`) that walks a coding agent through adding these workflows to your repository: it checks your prerequisites, reads your `.posit` deployment file (or asks for the server URL and content GUID), asks whether you're using Trusted Publishing or an API key, and writes the `deploy` (and optionally `cleanup-previews`) workflow for you. If you choose Trusted Publishing, it also offers to authorize the repository on your Connect server, working out the right subject claim format for you. Agent Skills are an open standard, so any agent that supports them (Claude Code, Codex, Cursor, Gemini CLI, and others) can use it by pointing at [`skills/setup-connect-deploy/`](skills/setup-connect-deploy/SKILL.md) in this repo. @@ -44,7 +44,7 @@ and adapts: | Feature | Minimum Connect version | On older servers | |---|---|---| | Deploying with an API key | broadly supported | — | -| Trusted Publishing (OIDC, no API key) | **2026.07.0**, plus an **Enhanced or Advanced** license | Login fails with a clear error---provide `connect-api-key` instead | +| [Trusted Publishing](#trusted-publishing) (OIDC, no API key) | **2026.07.0**, plus an **Enhanced or Advanced** license | Login fails with a clear error---provide `connect-api-key` instead | | Draft PR previews (`draft: true`, the default on pull requests) | **2025.07.0** | Deploy fails fast with a clear error---set `draft: false` to deploy directly instead of staging a preview | | Git provenance metadata (commit, author, branch, PR) | **2025.12.0** | Silently skipped; the deploy still succeeds, just without the metadata | @@ -53,6 +53,116 @@ confirm support: it skips git metadata (to avoid failing the upload) and does not pre-check drafts or Trusted Publishing, letting those attempts surface their own errors if the server is too old. +## Trusted publishing + +With [trusted publishing](https://docs.posit.co/connect/user/trusted-publishing/), +your workflow deploys to Connect without an API key: the job requests a +short-lived OIDC token from GitHub, and Connect exchanges it for a short-lived +credential scoped to the single content item you authorized. In your workflow all +this takes is `id-token: write` on the job; the rest is configured once on Connect. + +Authorize this repository on the content's **Access** settings: in the **Trusted +Publishing** section, select **Add Trusted Publisher**, choose the **GitHub +Actions** publisher type, and fill in: + +* **Repository** --- the repository segment of the `sub` (subject) claim in the + OIDC tokens your repository issues. See [Immutable subject + claims](#immutable-subject-claims) below for how to determine this; it is not + always just `owner/repo`. +* **Audience** --- leave it at the default `connect`, which is what the actions + request. (If you change it, pass the same value as the `audience` input.) + +You must be the content's owner or a collaborator who can change its settings. + +### Immutable subject claims + +A GitHub OIDC token identifies the workflow that requested it with a `sub` claim +like: + +``` +repo:my-org/my-repo:ref:refs/heads/main +``` + +Repositories created on or after July 15, 2026 instead use [immutable subject +claims](https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/), +which append GitHub's numeric owner and repository IDs so that the claim can't be +reused by a repository that later takes the same name: + +``` +repo:my-org@123456/my-repo@789012:ref:refs/heads/main +``` + +Older repositories keep the name-only format unless they opt in (repository +**Settings → Actions → General**, or the equivalent org-level setting), and +repositories renamed or transferred after that date adopt the new format too. + +Connect accepts either form in the **Repository** field, but it has to match what +your repository actually produces. If it doesn't, the token exchange fails with an +HTTP 400 and the deploy action reports a Trusted Publishing login failure, even +though the configuration looks right. + +The reliable way to get the value is to ask GitHub for the repository's subject +claim prefix and strip the leading `repo:` (this needs admin access to the repo): + +```bash +gh api "repos/$ORG/$REPO/actions/oidc/customization/sub" \ + --jq '.sub_claim_prefix | ltrimstr("repo:")' +``` + +Without admin access, build the immutable form from the owner and repository IDs: + +```bash +# From within a local clone of the repository: +gh api "repos/$(gh repo view --json nameWithOwner -q .nameWithOwner)" \ + --jq '"\(.owner.login)@\(.owner.id)/\(.name)@\(.id)"' + +# Or, for any repository by name: +gh api "repos/$ORG/$REPO" \ + --jq '"\(.owner.login)@\(.owner.id)/\(.name)@\(.id)"' +``` + +Use that immutable form if the repository was created on or after July 15, 2026 +(`gh repo view --json createdAt`) or has opted in; otherwise use plain +`owner/repo`. + +### Configuring a trusted publisher from the command line + +Instead of using the Connect UI, you can add the trusted publisher with the +[`posit` CLI](https://github.com/posit-dev/posit-cli), which these actions use +under the hood. Log in with your own Connect account once---a trusted-publishing +credential can't manage trusted publishers, so this has to be a human +credential: + +```bash +posit connect login https://connect.example.com +``` + +Then bind the repository to the content item, where `REPOSITORY` is the value +determined above and `GUID` is the content GUID (the `id` field in your +`.posit/publish/deployments/*.toml`): + +```bash +posit connect api "v1/content/$GUID/trusted-publishers" \ + -H "Content-Type: application/json" --input - <- posit-dev/connect-actions. Use when a user wants to add, scaffold, or configure Connect deployment (production deploy, PR draft previews, and preview cleanup) in a repo's .github/workflows — including choosing - trusted publishing (OIDC) vs. API-key auth and wiring up secrets. + trusted publishing (OIDC) vs. API-key auth, authorizing the repository as a + trusted publisher on Connect, and wiring up secrets. --- # Set up Posit Connect deployment with connect-actions @@ -19,7 +20,8 @@ those previews when a PR closes. Work through the steps **in order**. Ask the user the questions as you reach them rather than assuming answers — the right workflow depends on how they authenticate and whether they want PR previews. Do not write any files until -Step 6. +Step 6. (Step 3a can change configuration on the Connect server, but only after +the user opts in to it.) --- @@ -37,6 +39,11 @@ GitHub remote. actions only run on GitHub-hosted repos and stop unless they want to proceed anyway (e.g. they'll add the remote later). +Steps 3 and 3a use the GitHub CLI (`gh auth status`) and the `posit` CLI +(installed with `uv tool install git+https://github.com/posit-dev/posit-cli`). +Neither is required to scaffold the workflows — check for them when you get +there, and fall back to telling the user what to do in the UI if they're missing. + Then explain the one prerequisite the skill can't do for them: > The `deploy` action **updates existing content** on Connect — it does not @@ -93,6 +100,8 @@ Connect server supports it. `permissions: id-token: write` so it can request an OIDC token, which the action exchanges for a short-lived Connect key. - Nothing goes in the workflow for auth beyond that permission. +- If the user picks this, **offer to authorize the repository on Connect for + them** — see Step 3a. Skip Step 3a for Option B. **Option B — API key in a repo secret.** - The user needs a Connect API key with at least **publisher** privileges (from @@ -104,6 +113,84 @@ Connect server supports it. - The workflow references it as `connect-api-key: ${{ secrets.CONNECT_API_KEY }}` and does **not** need `id-token: write`. +## Step 3a — Authorize the repository on Connect (Trusted Publishing only) + +Ask whether the trusted publisher is already configured for this content. If it +is, skip ahead. Otherwise offer to configure it now — you can do the whole thing +from the command line, but it needs the user's *own* Connect credentials (a +trusted-publishing credential cannot manage trusted publishers) and they must be +the content's owner or a collaborator who can change its settings. + +**1. Determine the `repository` value.** This is the repository segment of the +`sub` (subject) claim in the OIDC tokens the repo issues, and it is *not* always +`owner/repo`. Repositories created on or after **July 15, 2026** use [immutable +subject claims](https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/), +which embed GitHub's numeric owner and repo IDs (`my-org@123456/my-repo@789012`); +older repos can opt in, and renames or transfers after that date also switch the +format. Getting this wrong is the classic failure: the config looks right but the +token exchange fails with an HTTP 400. + +Ask GitHub directly rather than guessing — this reports exactly what the repo +produces: + +```bash +gh api "repos/$ORG/$REPO/actions/oidc/customization/sub" +# {"use_default":true,"use_immutable_subject":false,"sub_claim_prefix":"repo:my-org/my-repo"} +``` + +The `repository` value is `sub_claim_prefix` with the leading `repo:` stripped. +If `sub_claim_prefix` doesn't start with `repo:` (a repo with a customized +subject claim template), the `github-actions` publisher type can't match it — +tell the user they need a **Custom OpenID Connect** publisher instead, and stop +there rather than registering something that won't work. + +That endpoint needs admin access to the repo. If it fails with 403/404, fall +back: get the immutable identifiers and the creation date, and use the immutable +form when the repo was created on or after 2026-07-15 (otherwise plain +`owner/repo`), telling the user which you chose and why. + +```bash +gh api "repos/$ORG/$REPO" --jq '"\(.owner.login)@\(.owner.id)/\(.name)@\(.id)"' +gh repo view "$ORG/$REPO" --json createdAt -q .createdAt +``` + +**2. Log in to Connect** as the user, if they aren't already +(`posit connect list` shows saved servers). This is interactive, so have *them* +run it — in Claude Code they can prefix with `!`: + +```bash +posit connect login +``` + +**3. Add the publisher**, using the content GUID from Step 2: + +```bash +posit connect api "v1/content/$GUID/trusted-publishers" \ + -H "Content-Type: application/json" --input - <` if they have more than one +saved. A 200/201 returns the service principal; adding the same workload twice +reuses the existing identity rather than duplicating it. Useful follow-ups: + +```bash +posit connect api "v1/content/$GUID/trusted-publishers" # list +posit connect api "v1/content/$GUID/trusted-publishers/$SP_GUID" -X DELETE # revoke +``` + +If this fails, report the error rather than working around it: a 402 means the +server lacks the Enhanced/Advanced license, a 403 means the account can't change +this content's permissions, and a 404 on the path itself means the server is +older than 2026.07.0. In each of those cases, recommend switching to Option B +(API key) instead. + ## Step 4 — Check requirements / dependency files Connect needs to know your app's dependencies. Check the app directory: @@ -247,8 +334,11 @@ jobs: After writing the files, summarize what was created and the remaining manual steps, which depend on the auth choice: -- **OIDC:** enable Trusted Publishing for this content on Connect (Access tab), - tied to this GitHub repo, if not already done. +- **OIDC:** if Step 3a authorized the repository, say so and note the + `repository` value that was registered (so a later repo rename is a known + breaking change). If it was skipped, the remaining step is to enable Trusted + Publishing for this content on Connect (Access tab), tied to this GitHub repo, + using the subject-claim format from Step 3a. - **API key:** confirm the `CONNECT_API_KEY` secret is set on the repo. Then suggest committing the workflow(s) and opening a pull request to exercise