Skip to content

Set up GitHub Actions deploys with harper deploy setup=true provider=github-actions - #3127

Merged
dawsontoth merged 14 commits into
mainfrom
feat/deploy-setup-github-actions
Oct 13, 2026
Merged

dawsontoth merged 14 commits into
mainfrom
feat/deploy-setup-github-actions

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

⊙ Problem

A GitHub Actions workflow can deploy with its OIDC identity token instead of a stored credential, but the cluster needs a deploy role, a user, and a trust policy whose claims match the workflow exactly. Writing those by hand is error-prone, and a mismatch shows up only as a 401 in CI.

❓ Your call: does this belong in core?

It is a provider on harper deploy setup=true, at the maintainer's direction, instead of HarperFast/create-harper#145's script in every template. A "no" moves it back into create-harper as a script; the provider is additive, so removing it is easy.

💡 Solution

harper deploy setup=true provider=github-actions project=my-app, run once from the repository's checkout:

  • creates a role and user that can only deploy, wait for the rollout and check the version;
  • creates a trust policy pinned to the repository, the workflow on main, and the production environment;
  • checks that policy against the workflow file first, and refuses a workflow that could never use it;
  • sets the repository's HARPER_CLI_TARGET variable if it has none.

It only creates. A rerun changes nothing, and a record or variable that already differs stops it before it writes.

⚖️ Alternatives

  • A setup script in each create-harper template — a fix would mean regenerating every project, and Studio templates would carry an orphan file.
  • A server operation that creates role, user and policy together — the GitHub half is client-side anyway, and the cluster half is three existing operations.
  • Reconcile existing records to the desired state — could repurpose a role another app uses.

❓ Your call: is the policy-write race acceptable?

Setup never writes an existing policy, so it can't undo a disable or a drop. A policy with the same id that another super user creates in the one round trip before the write would be replaced. A no-replace flag on add_oidc_trust would narrow that, not close it, since hdb_oidc_trust replicates.

❓ Your call: should a failed repository lookup refuse?

workflow_ref is compared exactly, so setup looks the repository up for GitHub's spelling. When that fails (a private repo without gh), it proceeds with the name as typed and says so. Refusing would block that case entirely.

🔧 Changes

✅ Verification

  • Unit tests: reconcile decisions, workflow checks and raw arguments, with the deploySetup and cliOperations suites: 153 passing.
  • End to end against a local build with a mock OIDC issuer: a create-harper project's deploy:setup-ci, then its workflow's own deploy steps with no stored credential. The exchange authenticated, the release was certified, and get_job returned COMPLETE.
  • The integration test drives the CLI against a real Harper (fixture, schema). It needs the loopback alias pool, so it runs in CI only.
  • Not run: a token from GitHub's own issuer, or a rollout across several nodes.
Every case run live, and the local gate results
  • Setup: the lookup corrected acme/my-app to GitHub's Acme/My-App; the scaffolded deploy.yaml passed the check; a rerun changed nothing; no gh exits 1 with the command; a widened role and an inactive user are refused and left as they were; a workflow on another branch, or without id-token: write, is refused before any write; branch=1.0 is pinned as typed; credentials in the target URL authenticate every call; a differently spelled workflow path pins the canonical one.
  • Variable: absent is created with one POST; one naming this cluster without the trailing slash is kept; one naming another cluster, including one created between the read and the POST, is left with exit 1 and its user:password@ removed.
  • A checkout of acme/my-app with repo=acme/other skips the workflow check with a note.
  • The exchange: the policy copied under the mock issuer (only issuer changed, plus the sub the generic profile requires). The same token was refused describe_all. A token from another branch or environment, or after drop_oidc_trust, was refused, and the deploy failed.
  • Integration cases: the stored records, a conflicting role refused and left alone, credentials in the target URL.
  • macOS gates: npm run lint and test:unit:resources (4071) pass. test:unit:main hangs at components/applicationSpawn.test.js and fails six files, exactly as a clean origin/main build does here; CI's Ubuntu run is the gate for them.

🤖 Generated with Claude Code

Release target: v5.4 — new CLI surface, not a backport; HarperFast/create-harper#147 needs it in 5.4.0
Related PRs: none found (partial scan: 3 PRs merged since 2026-10-08 beyond the first 50 were not scanned)
Complexity: medium

Review-Coverage: authored=claude; ran=gemini,cursor-muse,codex,cursor-composer; adjudicated=domain; blocked=cursor-grok(failed),claude(fallback)(out-of-budget); declined=cursor-kimi; rounds=8; full=3 @ abe0935

Review-Attention: read ~8m (decisions: do-less-alternative, variable-create-only, workflow-check-hard-refusal, checkout-identity-gate, client-composition-vs-server-create-only, exact-operation-set-match, cluster-wide-deploy-role) @ abe0935

dawsontoth and others added 8 commits October 8, 2026 16:48
…=github-actions`

A GitHub Actions workflow can deploy with its OIDC identity token instead of a
stored credential, but only under a trust policy that matches its token claim for
claim; a mismatch reaches CI as a bare 401. This adds the one-time setup to the
existing `harper deploy setup=true` flow (HarperFast/create-harper#145):

- derives the audience from the resolved target exactly as the CLI normalizes it,
  pins that target for the rest of the run, and takes the repository from repo=
  or the origin remote and its numeric id from `gh`, the public API, or
  repository_id=
- checks the branch and environment against the workflow file in the checkout,
  refusing a pair no run of it could present
- creates a deploy-only role and user (`<component>-ci-deploy`, with
  deploy_component and get_job) and the policy `github-actions-<component>`, only
  where missing: a matching record is left alone, and one that differs or was
  deactivated stops setup before it writes, so it can neither repurpose a role
  nor undo a revocation
- reads the policy back and compares it claim for claim, then sets the
  HARPER_CLI_TARGET repository variable with `gh`, or exits non-zero with the
  command to run
- needs no secret custody, so it branches before get_secrets_public_key

buildRequest keeps each argument as typed on a non-enumerable `_rawArgs`, so
`branch=1.0` is not pinned as "1".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the pre-push review of the GitHub Actions setup:
- an existing role that sets structure_user or cluster_user no longer reads as
  a match
- an on.push.branches pattern (`**`, `release/*`) is reported as unverified
  instead of as a mismatch, since GitHub matches those as globs
- when the repository was not looked up (an explicit repository_id=), setup
  says its name casing was not verified, since workflow_ref is compared exactly
- workflow= naming a directory is refused plainly instead of failing with
  EISDIR
- the unreachable "created before the failure" catch is replaced by a line
  before any write saying a rerun finishes a partial setup, since cliOperations
  exits rather than throwing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… repository up

From the second pre-push review of the GitHub Actions setup:
- only a saved-login target is pinned for the run; an explicit or environment
  target can't change mid-run, and pinning its normalized form dropped
  credentials carried in its userinfo
- the repository is looked up even with repository_id=, so workflow_ref uses
  GitHub's spelling of the name; a looked-up id that differs is refused
- workflow= is normalized to the repository-relative path GitHub names, used for
  both the file check and the claim, and must be in .github/workflows/
- branch patterns using `+`, and a branch only workflow_dispatch can reach, are
  reported as unverified rather than refused
- the policy list is read again just before add_oidc_trust, which replaces, so a
  policy created meanwhile stops setup; the residual window needs a server-side
  guard
- an unreadable workflow file is a CLI error, and a literal `_rawArgs=`
  argument no longer throws

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n test an impossible repository

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nvironment

From the closing pre-push review:
- a workflow whose push filter leaves out the branch, but that a schedule,
  repository_dispatch, workflow_run or workflow_dispatch run can reach on it,
  is reported as unverified rather than refused
- a workflow in which no job sets an environment is refused, since GitHub omits
  the environment claim and the policy could never match; one that calls a
  reusable workflow stays unverified, since the called file can set it
- the module header points at security/DESIGN.md instead of repeating it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…text

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er keeps its spelling

The default schema reads `branches: [1.0]` as the number 1, so comparing it as
text afterwards still lost the "1.0" GitHub matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth dawsontoth added this to the v5.4 milestone Oct 9, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for GitHub Actions OIDC trusted publishing via the harper deploy setup=true provider=github-actions command. It adds a new module to check workflow files, reconcile roles, users, and trust policies, and configure repository variables. It also preserves raw CLI arguments to prevent numeric-looking branch names from being parsed as numbers. Feedback highlights that the fallback GitHub API fetch request in bin/deploySetupGithubActions.ts lacks a required User-Agent header, which will cause the API to reject the request with a 403 error when the gh CLI is unavailable.

Comment thread bin/deploySetupGithubActions.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`harper deploy` checks the target's version with registration_info before
it uploads. The role setup creates refused it, so the deploy still worked
(the check assumes a current server when refused) but the cluster logged a
denied operation on every CI deploy. registration_info returns only
Harper's version, and any role without an operations allowlist may already
call it, so the role and the policy scope now list it.

Found by running a create-harper project's deploy job end to end against a
local build, with a mock OIDC issuer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dawsontoth added a commit to HarperFast/create-harper that referenced this pull request Oct 12, 2026
`deploy:setup-ci` now also grants registration_info, the version check
`harper deploy` makes before it uploads (HarperFast/harper#3127).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dawsontoth added a commit to HarperFast/documentation that referenced this pull request Oct 12, 2026
…turns

- commands.md: the role `harper deploy setup=true provider=github-actions`
  creates now also allows registration_info (HarperFast/harper#3127).
- deploying-from-ci.mdx and operations.md: the hand-written deploy role and
  policy scope list it too, since `harper deploy` checks the version with it
  first and an allowlist that omits it logs a denial on every deploy.
- operations.md: registration_info returns only `{ version, deprecated }`
  (since v5.0.0), not registration status, RAM or license details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed with Codex

if (plan.policy === 'create') {
// add_oidc_trust replaces a policy, so one created or disabled since the preflight would be
// overwritten. Checking again narrows that window; it can't close it without a server-side guard.
if (await policyNow()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major — A concurrent policy change can be overwritten, undoing a revocation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setup can't undo a revocation. It writes a policy only when none exists, at the preflight and again just before the write; an existing policy that differs or is disabled stops setup before it writes anything. Deactivating the user also holds, because setup refuses an inactive user and add_oidc_trust rejects a policy that names one. The remaining window is a policy with the same id created by someone else in the one round trip before the write, which setup would replace. add_oidc_trust could take a no-replace flag (Table.create 409s), but hdb_oidc_trust replicates, so that check is per node and narrows the window rather than closing it. A 5.3 cluster would also ignore the flag. I've left it as documented in security/DESIGN.md, and corrected "creates or disables" to "creates" there and in the docs (abe0935, HarperFast/documentation#722).

🤖 Posted by Claude Code

Comment thread bin/deploySetupGithubActions.ts Outdated
Comment thread bin/deploySetupGithubActions.ts
Comment thread bin/deploySetupGithubActions.ts Outdated
dawsontoth and others added 3 commits October 12, 2026 20:13
…ight workflow

From review:

- The repository variable is now created like the cluster records: setup
  reads it first, keeps one that already names this cluster, and leaves one
  that names another cluster (exit 1, with the gh command to change it).
  Replacing it moved every workflow in the repository that reads it.
- The local workflow file is only checked when the checkout is the
  repository the policy is for. Another repository's file could refuse a
  workflow that matches, or pass one that doesn't; it is now a note.
- The workflow check requires a job in the environment to grant
  `permissions: id-token: write` (job-level permissions replace the
  workflow's; GitHub's defaults grant none). Without it the CLI can't
  request a token, so the policy would never be used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… token

From the review round on the previous commit:

- The variable is created with `gh api -X POST .../actions/variables`,
  which doesn't replace an existing one, instead of `gh variable set`,
  which does. One created between setup's read and its write is read again
  and judged like any other: kept if it names this cluster, left otherwise.
- The value printed when a variable names another cluster has any
  `user:password@` removed.
- When a job calls a reusable workflow, a missing `id-token: write` on the
  jobs in the environment is reported as unverified, not refused: the
  called workflow can set the environment and request the token itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t on it

`admin:pass@host:9925` parses as a URL whose scheme is `admin:`, so the
password survived; a pattern now drops `user:password@` with or without a
scheme. withoutCredentials had also landed between variableAction and its
doc comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dawsontoth added a commit to HarperFast/documentation that referenced this pull request Oct 13, 2026
…eckout rule

Follows HarperFast/harper#3127's review changes: setup leaves a
HARPER_CLI_TARGET that already names another cluster, requires a job in
the environment to grant `id-token: write`, and checks the local workflow
only when the checkout is the repository the policy is for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setup writes a policy only when none exists at the preflight and at the
re-check just before the write, and refuses one that is disabled, so it
can't undo a disable or a drop. What the last round trip can overwrite is
a policy another super user creates with the same id, enabled or not. A
server-side create-only write would narrow that, not close it:
hdb_oidc_trust replicates, so the check would be per node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dawsontoth added a commit to HarperFast/documentation that referenced this pull request Oct 13, 2026
Setup never writes a policy that already exists, so another super user's
disable can't be overwritten; only a policy with the same id created in
the moment before setup writes it can be (HarperFast/harper#3127).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ethan-Arrowood pushed a commit to HarperFast/documentation that referenced this pull request Oct 13, 2026
…a workflow uses a trust policy (#722)

* Document `harper deploy setup=true provider=github-actions`, and how a workflow uses a trust policy

- commands.md: a GitHub Actions deploys (OIDC) section for the new setup
  provider: what it checks and creates, that it only creates, its names, and
  how to revoke; `setup=true` mentions it
- authentication.md: a "Using it in a workflow" subsection that says plainly
  that nothing in a workflow names the policy, and every `harper` command in a
  job authenticates through it when four conditions hold, with a one-job excerpt
- operations.md and the CI guide point at both, and the guide notes the setup
  command suits branch-triggered workflows, not its tag-triggered ones
- 5.4 release notes: the new setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Match the setup steps to the final provider: the repository is always looked up, and what the workflow check refuses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Close the setup tip, and say authentication is not authorization

From the pre-push review:
- the CI guide's :::tip closing marker sat on the paragraph's last line, so the
  admonition never closed; it now has a line of its own
- "every harper command authenticates" now says each may do only what the
  policy's user's role allows, and the guide notes setup's role lacks the
  get_deployment the manual role adds
- condition 4 lists every credential that outranks the exchange, and links the
  precedence order
- commands.md says a missing workflow file leaves the whole check unverified,
  that a concurrent writer can still overwrite the policy, and what to do when
  setting the variable fails; every page names the role setup creates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Qualify the release note's workflow check and create-only claim, and say a rerun sets the variable again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Note v5.4.0 in plain text where it sits mid-sentence, keeping the badge for headings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* List registration_info in the CI deploy roles, and correct what it returns

- commands.md: the role `harper deploy setup=true provider=github-actions`
  creates now also allows registration_info (HarperFast/harper#3127).
- deploying-from-ci.mdx and operations.md: the hand-written deploy role and
  policy scope list it too, since `harper deploy` checks the version with it
  first and an allowlist that omits it logs a denial on every deploy.
- operations.md: registration_info returns only `{ version, deprecated }`
  (since v5.0.0), not registration status, RAM or license details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep registration_info's other mentions consistent, and qualify the 5.4 note

- operations.md: `get_fingerprint` no longer points at registration_info,
  which returns only the version.
- operations-api-commands.md: registration_info gets the Harper version.
- authentication.md: the example deploy role lists registration_info, like
  the role setup creates.
- 5.4 release note: a rerun sets the variable again, and a policy another
  super user changes just before setup writes it is still overwritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Say to pass workflow= when the workflow file isn't deploy.yaml

Setup defaults to .github/workflows/deploy.yaml, create-harper's name,
while the CI guide and the operations examples use deploy.yml. A missing
default file is only reported as unverified, so setup could still pin
deploy.yaml and leave a deploy.yml workflow's token unable to match. The
reference now says so next to the command, with a workflow= example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Describe setup's create-only variable, the id-token check, and the checkout rule

Follows HarperFast/harper#3127's review changes: setup leaves a
HARPER_CLI_TARGET that already names another cluster, requires a job in
the environment to grant `id-token: write`, and checks the local workflow
only when the checkout is the repository the policy is for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Say GITHUB_REPOSITORY, when set, names the checkout's repository

Setup takes the checkout's repository from GITHUB_REPOSITORY before the
origin remote, so the checkout rule now says both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Say the setup race can only replace a policy created in the window

Setup never writes a policy that already exists, so another super user's
disable can't be overwritten; only a policy with the same id created in
the moment before setup writes it can be (HarperFast/harper#3127).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth
dawsontoth merged commit 1070466 into main Oct 13, 2026
56 checks passed
@dawsontoth
dawsontoth deleted the feat/deploy-setup-github-actions branch October 13, 2026 20:46
dawsontoth added a commit to HarperFast/create-harper that referenced this pull request Oct 14, 2026
`deploy:setup-ci` now also grants registration_info, the version check
`harper deploy` makes before it uploads (HarperFast/harper#3127).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot pushed a commit to HarperFast/create-harper that referenced this pull request Oct 14, 2026
## [1.13.0](v1.12.12...v1.13.0) (2026-10-14)

### Features

* deploy on merge to main with GitHub OIDC instead of a stored refresh token ([20b31b5](20b31b5)), closes [#144](#144) [#145](#145)

### Bug Fixes

* comment out a placeholder HARPER_CLI_TARGET in the scaffolded .env ([7ae2954](7ae2954))
* keep numeric directory names from reaching harper as numbers, and name a failed deploy ([9399a14](9399a14))
* name the component after the project directory, and order deploys with workflow concurrency ([95b9395](95b9395))
* refuse to re-run a deploy of a commit that is no longer the branch tip ([d1eff59](d1eff59))
* skip superseded deploys, and sanitize the project name before it reaches shell commands ([a82bc11](a82bc11))
* **studio:** drop a Deployment section that ends the README or has CRLF line endings ([0322a83](0322a83))
* **templates:** list @harperfast/schema-codegen in the vanilla templates' dependencies ([945344f](945344f))
* **templates:** write the rollback placeholder as DEPLOYMENT_ID, not <id> ([bbbfd6a](bbbfd6a))

### Documentation

* **templates:** list registration_info in the deploy role's operations ([4994f20](4994f20)), closes [HarperFast/harper#3127](HarperFast/harper#3127)

### Tests

* read the deploy workflows with LF line endings, since a Windows checkout has CRLF ([6417bdc](6417bdc))
dawsontoth added a commit to HarperFast/skills that referenced this pull request Oct 14, 2026
Regenerate the deploy rules against documentation@81c1b0e, which documents
`harper deploy setup=true provider=github-actions` (HarperFast/harper#3127,
HarperFast/documentation#722). deploying-from-ci now leads with that setup
and the four conditions under which a job's harper commands authenticate,
and keeps the hand-written role and policy for tag releases and
customization, with `registration_info` added to the role.
creating-harper-apps covers the deploy workflow create-harper 1.13.0
scaffolds (HarperFast/create-harper#147): deploy:setup-ci once, merge to
main to deploy, npm run deploy by hand, and going back by deployment_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dawsontoth added a commit to HarperFast/skills that referenced this pull request Oct 14, 2026
Regenerate the deploy rules against documentation@81c1b0e, which documents
`harper deploy setup=true provider=github-actions` (HarperFast/harper#3127,
HarperFast/documentation#722). deploying-from-ci now leads with that setup
and the four conditions under which a job's harper commands authenticate,
and keeps the hand-written role and policy for tag releases and
customization, with `registration_info` added to the role.
creating-harper-apps covers the deploy workflow create-harper 1.13.0
scaffolds (HarperFast/create-harper#147): deploy:setup-ci once, merge to
main to deploy, npm run deploy by hand, and going back by deployment_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot pushed a commit to HarperFast/skills that referenced this pull request Oct 14, 2026
## [1.15.0](v1.14.3...v1.15.0) (2026-10-14)

### Features

* **harper-best-practices:** deploy from CI with OIDC, and roll back by deployment_id ([b2f99fc](b2f99fc)), closes [HarperFast/documentation#711](HarperFast/documentation#711)
* **harper-best-practices:** set up CI deploys with one command ([8b6092e](8b6092e)), closes [HarperFast/harper#3127](HarperFast/harper#3127) [HarperFast/documentation#722](HarperFast/documentation#722) [HarperFast/create-harper#147](HarperFast/create-harper#147)

### Bug Fixes

* **harper-best-practices:** anchor deploying-from-ci on the pinned CLI install ([5490f3f](5490f3f))
* **harper-best-practices:** keep cluster credentials out of GitHub secrets ([6c39753](6c39753))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants