Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 114 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 |

Expand All @@ -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 - <<JSON
{
"name": "GitHub Actions ($REPOSITORY)",
"template": "github-actions",
"config": {"repository": "$REPOSITORY", "audience": "connect"}
}
JSON
```

Adding the same workload again reuses the existing identity rather than creating
a duplicate. To see what's authorized, or to revoke it:

```bash
posit connect api "v1/content/$GUID/trusted-publishers"
posit connect api "v1/content/$GUID/trusted-publishers/$SP_GUID" -X DELETE
```

The bundled [Agent Skill](#set-up-with-an-agent-skill) can do all of this for
you, including reading the subject claim format from GitHub.

## Actions

### `deploy` - Deploy to Posit Connect
Expand Down Expand Up @@ -87,7 +197,7 @@ The action requires two things at minimum: the destination to deploy to (Connect

To identify the destination, you can either provide `connect-server` and `content-guid` directly as arguments, or you can provide a path to the `deployment-file`, the TOML file written out by Posit Publisher. If you omit all three of these, the action will look in the `.posit/publish/deployments/` directory and if there is a single deployment file in there, it will use that. If there are zero deployment files or more than one, you will need to provide the URL and GUID as arguments to the action.

For authentication, we recommend using Trusted Publishing if your Connect server supports it. You do not need to provide any secrets for this to work, once you have enabled it for your content on your Connect server, but you do need to add `id-token: write` to the `permissions` block of your workflow job. If Trusted Publishing is not an option, you can provide `connect-api-key`, which should point to `${{ secrets.CONNECT_API_KEY }}` or similar---do not enter an API key in your workflow file directly.
For authentication, we recommend using [Trusted Publishing](#trusted-publishing) if your Connect server supports it. You do not need to provide any secrets for this to work, once you have enabled it for your content on your Connect server, but you do need to add `id-token: write` to the `permissions` block of your workflow job. If Trusted Publishing is not an option, you can provide `connect-api-key`, which should point to `${{ secrets.CONNECT_API_KEY }}` or similar---do not enter an API key in your workflow file directly.

#### Requirements files

Expand Down
98 changes: 94 additions & 4 deletions skills/setup-connect-deploy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ description: >-
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
Expand All @@ -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.)

---

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 <server-url>
```

**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 - <<JSON
{
"name": "GitHub Actions ($REPOSITORY)",
"template": "github-actions",
"config": {"repository": "$REPOSITORY", "audience": "connect"}
}
JSON
```

Confirm the exact JSON with the user before sending it. `posit connect api` uses
the default saved server, so add `-s <server-url>` 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:
Expand Down Expand Up @@ -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
Expand Down
Loading