Skip to content

fix(ci): Keep the Forgejo Access service token across a teardown - #895

Merged
stefanko-ch merged 3 commits into
mainfrom
fix/preserve-forgejo-service-token
Sep 19, 2026
Merged

stefanko-ch merged 3 commits into
mainfrom
fix/preserve-forgejo-service-token

Conversation

@stefanko-ch

@stefanko-ch stefanko-ch commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Closes #892.

What broke

Lifecycle: Teardown runs an untargeted tofu destroy, and the Cloudflare Access service token for Forgejo lives in that state. Every teardown destroyed it; the next spin-up minted a replacement with a new client id and secret. Anything outside the stack authenticating to Forgejo with that pair stopped working at that moment — and stopped working silently, because Access answers an unauthenticated call with a 302 to its login page rather than a 401.

Measured on the Conductor-Stack after its update to v0.82.2 (teardown → update → spin up):

Forgejo API redirected (302) on GET /repos/nexus-conductor/Nexus-Stack-template/releases/latest
— refusing to follow.

Two Nexus-Conductor operations failed, retried, failed again. Nothing was wrong with the stack; the credential it had been given no longer existed.

Approach

The token is a management-plane credential: its lifetime is the relationship between the two systems, not the lifetime of a server. So it is taken out of the state before the destroy and imported back on the next spin-up — the trick setup-control-plane.yaml:950 already uses for the KV namespace.

The alternative, moving it into tofu/control-plane, was considered and dropped: the Access application it hangs on lives in the stack state, and the secret would then be created in a layer with no path to Infisical — the server may not exist at Setup Control Plane time. That is a new delivery mechanism for a one-time value, which is the shape of the original problem.

.github/scripts/forgejo-service-token.sh, three verbs:

Verb Where What
preserve teardown.yml, before the destroy tofu state rm, so the destroy cannot reach it. Fails loudly rather than let the destroy proceed.
adopt spin-up.yml, before the apply Finds the token by the name main.tf gives it, imports it, then plans that one resource with -detailed-exitcode and refuses if applying would still change it.
purge destroy-all.yml, after the destroy Removes it by name — by then it is exactly the unmanaged object a teardown left behind.

Only the rebuild lifecycle needed this. teardown-snapshot.yml destroys -target=hcloud_server.main and nothing else, so the token was never at risk there. A test fails if that ever widens.

Three refusals, each deliberate

  • Two tokens of the same name → adopt stops. Importing the wrong one hands the Access policy a credential the caller does not hold: the same silent failure, one level deeper.
  • A listing that returns 200 with "success": false → an error, not an empty list. Read as "no token", it would mint a second one.
  • An import that succeeds but does not settle → the -detailed-exitcode plan catches it. The credential would otherwise rotate on the apply anyway, with nothing saying so.

What cannot be restored, and why that is fine

The client_secret. Cloudflare returns it once, at creation; the provider documentation for v4.49 is explicit that an imported token "will not have the client_secret available in the state for use".

That is the point rather than a gap: nobody needs a new one, because the external system keeps the pair it was given. It does mean Infisical shows the id and no secret after the first teardown — _filter_empty drops the empty value rather than writing a blank over anything. The adopt step says so in the log, and three docs pages say it too, because unexplained it reads as a defect.

Guards

tests/unit/test_forgejo_service_token.py, 29 tests. The script runs for real under bash with tofu and curl replaced by fakes, so what is asserted is which subcommands ran with which arguments — not which strings the file contains. jq is the one real dependency, because the name matching is the part worth exercising; locally its absence skips, in CI it fails.

Seven mutations, each caught by the intended test:

Mutation Caught by
preserve step removed from teardown.yml test_the_rebuild_teardown_preserves_the_token_before_it_destroys
preserve step moved after the destroy same
adopt step removed from spin-up.yml test_the_spin_up_adopts_the_token_before_it_applies
DOMAIN dropped from the adopt step's environment test_the_spin_up_passes_what_the_adopt_step_reads
purge removed from destroy-all.yml test_destroy_all_removes_the_preserved_token
the snapshot teardown made untargeted test_the_snapshot_teardown_needs_no_preservation
the SKIP_TOFU_DESTROY gate re-added to purge test_the_purge_step_does_not_depend_on_the_opentofu_backend

One near-miss worth recording. The first version of the ordering tests used str.find(), which returns -1 for a needle that does not match — and -1 sorts before everything, so assert preserve < destroy could never have failed. The needles did not match, because the step calls the script through "$GITHUB_WORKSPACE/...". They are regexes now, with an explicit assert that each one matched.

pytest tests/unit: 3593 passed. Pre-commit (ruff, mypy strict, actionlint): all hooks pass.

Docs

  • docs/stacks/forgejo.md — a new section on the token's lifetime, the once-only secret, and the two refusals.
  • docs/concepts/lifecycle.md — the token joins the "always survives" list.
  • docs/admin-guides/setup-guide.md — one sentence on the ENABLE_FORGEJO_SERVICE_TOKEN row.
  • docs/admin-guides/troubleshooting.md — what to do when the adopt step refuses two same-named tokens.

Not verified yet

A real teardown → spin-up cycle. The script's logic is exercised by the fakes, but Cloudflare's own import behaviour is not: whether tofu import of this resource leaves a plan that is genuinely empty is stated by the provider's docs and checked at runtime by the -detailed-exitcode guard, rather than measured here. The next Conductor lifecycle is what settles it, and the guard is what makes a wrong answer loud instead of silent.

Side finding, filed separately

The shellcheck hook matches ^(scripts|stacks)/.*\.sh$, so .github/scripts/ is not linted at all — which is how the new script got a "no files to check". Measured the cost of widening it: seven of eight scripts are clean, and the eighth has one false positive (a function reached only through trap) and one genuinely dead MIGRATION_SQL variable that is a stale copy of a schema migration. Filed as #894 rather than folded in here.

Local CodeRabbit round

Reviewed c4022254: 1 finding, valid, fixed in 397cece5.

  • Fixed (major): the purge step had copied its neighbours' if: env.SKIP_TOFU_DESTROY != 'true'. That flag means the OpenTofu backend could not be reached — missing R2 credentials, or a state bucket already gone. A preserved token is not in the state, so the flag says nothing about it, and the condition skipped the cleanup in exactly the case where nothing else could ever perform it.

397cece5 itself was not reviewed locally; this review covers it.

Summary by Sourcery

Keep the Forgejo Access service token stable across rebuild lifecycles while ensuring complete destruction still removes preserved credentials.

New Features:

  • Preserve and restore the Forgejo Cloudflare Access service token across rebuild teardowns and subsequent spin-ups.
  • Remove preserved Forgejo service tokens during complete destroy workflows.

Bug Fixes:

  • Prevent external Forgejo API clients from silently losing authentication when stack teardown previously destroyed and recreated the service token.

Enhancements:

  • Add safeguards that refuse ambiguous token adoption, invalid Cloudflare responses, failed imports, or plans that would rotate the credential.
  • Document the token lifecycle, one-time client secret behavior, configuration implications, and troubleshooting steps.

Documentation:

  • Document Forgejo service-token persistence, one-time secret availability, lifecycle behavior, setup configuration, and ambiguous-token recovery.

Tests:

  • Add integration-style unit coverage for the token preservation, adoption, purge, error handling, and workflow ordering contracts.

`Lifecycle: Teardown` runs an untargeted `tofu destroy`, and the Cloudflare
Access service token for Forgejo lives in that state. So every teardown
destroyed it and the next spin-up minted a replacement with a new client id
and secret. Anything outside the stack that authenticates to Forgejo with
that pair stopped working at that moment — and stopped working silently,
because Access answers an unauthenticated call with a 302 to its login page
rather than a 401. Measured on the Conductor-Stack after its update to
v0.82.2: two Nexus-Conductor operations failed, retried, failed again, with
nothing wrong except a credential that no longer existed.

The token is a management-plane credential. Its lifetime is the relationship
between the two systems, not the lifetime of a server. So it is taken out of
the state before the destroy and imported back on the next spin-up — the
trick `setup-control-plane.yaml` already uses for the KV namespace.

`.github/scripts/forgejo-service-token.sh` has the three halves:

  preserve  teardown.yml, before the destroy: `tofu state rm`, so the destroy
            cannot reach it. Fails loudly rather than let the destroy run.
  adopt     spin-up.yml, before the apply: finds the token by the name main.tf
            gives it and imports it. Then plans that one resource with
            `-detailed-exitcode` and refuses if applying would still change
            it — an import that succeeds but does not settle would rotate the
            credential anyway, and nothing would say so.
  purge     destroy-all.yml, after the destroy: removes it by name, because by
            then it is exactly the unmanaged object a teardown left behind.

Two refusals are deliberate. `adopt` stops when two tokens carry the name:
importing the wrong one hands the Access policy a credential the caller does
not hold, which is the same silent failure one level deeper. And a listing
that returns 200 with `"success": false` is an error, not an empty list — read
as "no token", it would mint a second one.

What this cannot restore is the client_secret. Cloudflare returns it once, at
creation; the provider's documentation says an imported token "will not have
the client_secret available in the state for use". That is the point rather
than a gap — nobody needs a new one, the external system keeps the pair it
was given. It does mean Infisical shows the id and no secret after the first
teardown, so the adopt step says so in the log and three docs pages say it too.

Only the rebuild lifecycle needed this: `teardown-snapshot.yml` destroys
`-target=hcloud_server.main` and nothing else, so the token was never at risk
there. A test fails if that ever widens.

Guards in tests/unit/test_forgejo_service_token.py, 28 of them. The script
runs for real under bash with `tofu` and `curl` replaced by fakes, so what is
asserted is which subcommands ran with which arguments. Six workflow-wiring
mutations were each caught by the intended test: the preserve step removed,
the preserve step moved after the destroy, the adopt step removed, DOMAIN
dropped from its environment, purge removed, and the snapshot teardown made
untargeted.

One near-miss worth recording: the first version of the ordering tests used
`str.find()`, which returns -1 for a needle that does not match — and -1 sorts
before everything, so `assert preserve < destroy` could never have failed.
The needles did not match, because the step calls the script through
`"$GITHUB_WORKSPACE/..."`. They are regexes now, with an explicit assert that
each one matched.

Closes #892
From the local CodeRabbit round on c402225, and valid for a stronger reason
than the one it gave.

The purge step copied its neighbours' `if: env.SKIP_TOFU_DESTROY != 'true'`.
That flag is set when the OpenTofu backend could not be reached — missing R2
credentials, or a state bucket that is already gone. A preserved service
token is not in the state; it is an object in the Cloudflare account, and the
step needs only the API, the account id and the domain. So the condition
skipped the cleanup in precisely the case where nothing else could ever
perform it: a stack torn down, its state bucket deleted, and a live
credential left in the account with no policy and no owner.

Guard added and mutation-tested: re-adding the condition fails
test_the_purge_step_does_not_depend_on_the_opentofu_backend.

Refs #892
@sourcery-ai

sourcery-ai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Keeps the externally consumed Forgejo Cloudflare Access service token stable across rebuild teardowns by taking it out of state before destruction and adopting it before the next apply, with defensive API/state checks, unconditional full-destroy cleanup, executable tests, and corresponding operator documentation.

Sequence diagram for preserving and adopting the Forgejo service token

sequenceDiagram
    participant Teardown as Rebuild Teardown
    participant Script as forgejo-service-token.sh
    participant State as OpenTofu State
    participant Cloudflare as Cloudflare Access
    participant SpinUp as Spin-Up

    Teardown->>Script: preserve
    Script->>State: tofu state rm RESOURCE
    Note over State,Cloudflare: Token remains in Cloudflare and is no longer destroyable
    Teardown->>State: tofu destroy
    SpinUp->>Script: adopt
    Script->>Cloudflare: List service tokens by token_name()
    Cloudflare-->>Script: One matching token ID
    Script->>State: tofu import RESOURCE
    Script->>State: tofu plan -target=RESOURCE -detailed-exitcode
    State-->>Script: No changes
    SpinUp->>State: tofu apply
Loading

Flow diagram for Forgejo service token safety checks

flowchart TD
    A["Lifecycle action"] --> B{"Operation"}
    B -->|preserve| C{"Token in OpenTofu state?"}
    C -->|yes| D["tofu state rm RESOURCE"]
    C -->|no| E["Continue without preservation"]
    D --> F["Run untargeted destroy"]
    B -->|adopt| G{"Feature enabled?"}
    G -->|no| H["Continue without adoption"]
    G -->|yes| I["List matching Cloudflare tokens"]
    I --> J{"Exactly one match?"}
    J -->|no| K["Refuse and report error"]
    J -->|yes| L["tofu import RESOURCE"]
    L --> M{"tofu plan -detailed-exitcode is unchanged?"}
    M -->|no| K
    M -->|yes| N["Proceed with apply"]
    B -->|purge| O["Delete all matching tokens from Cloudflare"]
Loading

File-Level Changes

Change Details Files
Preserve and restore the Forgejo Cloudflare Access service token across rebuild lifecycles.
  • Added preserve, adopt, and purge actions to remove the token from OpenTofu state before teardown, re-import one matching Cloudflare token before spin-up apply, and delete preserved tokens during full destruction.
  • Guarded adoption against ambiguous names, unsuccessful API listings, failed imports, and non-empty targeted plans; documented that imported tokens cannot restore the one-time client secret.
  • Wired preservation before rebuild teardown, adoption before spin-up apply, and unconditional purge after destroy-all; left targeted snapshot teardown unchanged.
.github/scripts/forgejo-service-token.sh
.github/workflows/teardown.yml
.github/workflows/spin-up.yml
.github/workflows/destroy-all.yml
Added real shell-level tests for token lifecycle behavior and workflow ordering.
  • Replaced tofu and curl with fakes while exercising the Bash script for state handling, API failures, duplicate detection, import stabilization, deletion, and secret-safety behavior.
  • Added workflow contract tests covering step ordering, required environment variables, purge independence from SKIP_TOFU_DESTROY, and the targeted snapshot teardown guard.
tests/unit/test_forgejo_service_token.py
Updated operator and lifecycle documentation for the token's new management-plane lifetime.
  • Explained token survival, one-time secret behavior, duplicate-token refusal, and manual cleanup semantics.
  • Added setup and troubleshooting guidance and listed the token among resources that survive rebuild teardown.
docs/stacks/forgejo.md
docs/concepts/lifecycle.md
docs/admin-guides/setup-guide.md
docs/admin-guides/troubleshooting.md

Assessment against linked issues

Issue Objective Addressed Explanation
#892 Ensure the Forgejo Cloudflare Access service token and its externally used credential pair survive the normal teardown and subsequent spin-up without being rotated. ✅
#892 Prevent silent credential replacement or ambiguous token adoption by preserving the token before destruction, safely importing it before apply, failing loudly on lookup/import/plan errors, and cleaning it up during a full destroy. ✅
#892 Document the token's lifecycle and the once-only availability of its client secret, including recovery guidance for ambiguous tokens. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 48 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: stefanko-ch/Nexus-Stack/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: d52a2cfa-158a-46c7-b1fb-bd036d314b40

📥 Commits

Reviewing files that changed from the base of the PR and between e3d9d1e and 648def2.

📒 Files selected for processing (9)
  • .github/scripts/forgejo-service-token.sh
  • .github/workflows/destroy-all.yml
  • .github/workflows/spin-up.yml
  • .github/workflows/teardown.yml
  • docs/admin-guides/setup-guide.md
  • docs/admin-guides/troubleshooting.md
  • docs/concepts/lifecycle.md
  • docs/stacks/forgejo.md
  • tests/unit/test_forgejo_service_token.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path=".github/workflows/destroy-all.yml" line_range="331-336" />
<code_context>
+      # is not in the state; it is an object in the Cloudflare account, and
+      # this step needs only the API. So the case where the flag is set is
+      # precisely the case where nothing else will ever remove it.
+      - name: Remove a preserved Forgejo service token
+        env:
+          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+          DOMAIN: ${{ secrets.DOMAIN }}
+        run: bash .github/scripts/forgejo-service-token.sh purge
+
       - name: Destroy Control Plane infrastructure
</code_context>
<issue_to_address>
**issue (bug_risk):** The purge step is skipped whenever the preceding destroy step fails, because GitHub Actions applies the default `success()` condition to this step. A failed or cancelled infrastructure destroy therefore leaves the preserved Cloudflare token behind even though `destroy-all` is intended to remove everything.

**Triggers:** When the OpenTofu destroy fails after a teardown has preserved the token.

**Suggested fix:** Run the purge step with `if: always()` (while retaining the required credentials), or otherwise ensure cleanup executes after a failed destroy.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and if this is wrong, the teardown/apply orchestration could rotate or fail to remove a Cloudflare Access service token, either breaking the external management plane or leaving a live credential behind after teardown. Reverting the change does not undo tokens already preserved or deleted, so cleanup and possible credential recovery would require manual intervention.

Blocking findings: .github/workflows/destroy-all.yml:336


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread .github/workflows/destroy-all.yml
@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

coverage

Coverage report — nexus_deploy
FileStmtsMissCoverMissing
__init__.py50100% 
_remote.py150100% 
cli.py40100% 
compose_restart.py400100% 
compose_runner.py880100% 
config.py1810100% 
firewall.py2060100% 
forgejo.py5985590%783–784, 789, 812–813, 825–826, 862–863, 875–876, 894–895, 920–921, 943–944, 955–956, 1011–1012, 1020–1021, 1026, 1032–1033, 1057–1058, 1091–1092, 1095, 1126–1127, 1168–1169, 1174–1175, 1215–1216, 1247–1248, 1271–1272, 1277–1278, 1377–1378, 1383–1384, 1860, 1864, 1885, 1913–1914, 2001
forgejo_runner.py47197%228
hetzner_capacity.py1720100% 
hetzner_snapshot.py2020100% 
infisical.py2220100% 
kestra.py177398%227, 441, 802
orchestrator.py6847788%197, 496–497, 509, 610, 802, 814, 984–985, 990–991, 1023–1025, 1034, 1039–1041, 1052, 1089–1090, 1095–1096, 1116, 1151–1152, 1157–1158, 1166, 1191–1192, 1200, 1271–1272, 1277–1278, 1330–1331, 1336–1337, 1588, 1591, 1661, 1667–1668, 1673–1674, 1708, 1832–1833, 1838–1839, 1888–1889, 1894–1895, 1954, 1969, 2026, 2031–2032, 2037–2038, 2045, 2051, 2226, 2233, 2245–2246, 2251–2252, 2258, 2264, 2348–2349, 2370–2371
pg_preflight.py191199%214
pipeline.py2361394%166–167, 351, 389, 470, 492, 587–588, 633–634, 724–725, 772
r2_tokens.py113298%87, 150
s3_persistence.py200199%315
s3_restore.py1030100% 
secret_sync.py990100% 
seeder.py980100% 
service_env.py5513394%2072, 2074–2076, 2084–2085, 2660–2663, 2668–2674, 2741–2745, 2761–2765, 2789, 2791, 2813–2814, 2821, 2946
services.py361199%2921
setup.py1651392%245, 315–318, 326, 330–335, 351
ssh.py560100% 
stack_sync.py960100% 
tfvars.py440100% 
tofu.py860100% 
workspace_coords.py1010100% 
TOTAL514120096% 

@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Address PR review comments on #895.

[4052605472] sourcery-ai — Fixed. The purge step carried no `if:`, and a
step without one gets an implicit `success()`: "A default status check of
success() is applied unless you include one of these functions" (GitHub's
expressions reference). So a destroy that failed skipped the cleanup — which
is precisely the run in which a preserved token is most likely to be left
behind, since nothing else reaches it.

Fixed with `if: ${{ !cancelled() }}` rather than the suggested `always()`.
The same page advises against `always()` because it runs after a human
cancels the workflow, and a cancelled destroy-all should change nothing. A
failed one still should: the operator asked for everything to go, and the
token is reachable through the API whether or not the state was.

The guard for this step now checks all three properties and is
mutation-tested on each: no condition at all, `always()`, and the
`SKIP_TOFU_DESTROY` gate each fail it.
@stefanko-ch
stefanko-ch merged commit 6784086 into main Sep 19, 2026
15 checks passed
@stefanko-ch
stefanko-ch deleted the fix/preserve-forgejo-service-token branch September 19, 2026 08:52
stefanko-ch pushed a commit that referenced this pull request Sep 19, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.82.3](v0.82.2...v0.82.3)
(2026-09-19)


### 🐛 Bug Fixes

* **ci:** Keep the Forgejo Access service token across a teardown
([#895](#895))
([6784086](6784086))
* **deploy:** Copy stacks without rsync when the job image has none
([#899](#899))
([d083531](d083531))
* **service_env:** reject empty DOMAIN in render_all_env_files
([#863](#863))
([#867](#867))
([5c6e93d](5c6e93d))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

## Summary by Sourcery

Release version 0.82.3 with fixes for CI token persistence, deployment
stack copying, and invalid domain configuration.

Bug Fixes:
- Preserve the Forgejo Access service token during teardown.
- Support copying deployment stacks when rsync is unavailable in the job
image.
- Reject empty DOMAIN values when rendering service environment files.

Chores:
- Release version 0.82.3 and update the changelog.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

fix(stacks): the Forgejo service token dies with every teardown, so every external management plane breaks silently

1 participant