Schedule unattended token refresh¶
Keep unattended posting alive. keryx auth refresh refreshes/rotates the stored
platform tokens and alerts when it can't — run it from a scheduled pipeline in the
owning project so a token never goes silently stale (the #1 unattended failure mode;
spec 0010-auth-refresh.md).
keryx is stateless and ships no schedule — the schedule lives in your project's CI
(the same repo that holds your content and runs keryx publish due). This guide wires that
up on GitLab.
Prerequisite: you've already captured each platform's token with
keryx auth <platform>— see the credential guides for Instagram, YouTube, TikTok, and LinkedIn.
What refresh does per platform¶
auth refresh all fans out across your enabled platforms, runs each independently,
reports per-platform, and exits non-zero if any needs attention (so the pipeline goes
red):
| Platform | Action | Persists? | Alerts when |
|---|---|---|---|
| refresh the long-lived token in place (near its ~60-day expiry) | new access token | refresh fails / token already dead | |
| YouTube | health-check the durable refresh token | nothing (durable) | refresh token revoked/invalid |
| TikTok | rotate — exchange for a new access and refresh token | new refresh token immediately | rotation fails (old refresh dead) |
| none possible (standard app has no refresh) | — | re-auth due within 14 days |
TikTok is the one that must write its rotated refresh token back, or the next
scheduled run is locked out — which is what the gitlab write-back backend (below) is
for.
1. Preview it first (--dry-run)¶
--dry-run asks each platform whether its token is still accepted, reports the days left
on a recorded expiry, and changes nothing. A rejected token shows as reauth-required
here too, so the preview is a real health check:
$ keryx auth refresh all --dry-run
instagram healthy expires 2026-11-19 token accepted; 59 days left
facebook healthy expires — token accepted
tiktok rotated expires — would rotate the refresh token
youtube healthy expires — would health-check the refresh token
expires — on a platform that does expire means no expiry is on record; a fresh
keryx auth <platform> records it.
You can scope to one platform — keryx auth refresh tiktok --dry-run. An unknown name
errors (no refresher for "…", exit 1).
2. Choose the write-back backend¶
Locally, refreshed tokens persist via keychain/config. In CI there's no keychain, and a
job can't natively write back to the CI variables it read — so select the gitlab
backend, which updates the CI/CD variables over the API. Select it from the job's
environment, not from a committed config key:
token-refresh:
variables:
KERYX_AUTH_WRITEBACK_BACKEND: gitlab # local (default) | gitlab
# project and api_url default to CI_PROJECT_ID / CI_API_V4_URL inside a pipeline:
# KERYX_AUTH_WRITEBACK_GITLAB_PROJECT, KERYX_AUTH_WRITEBACK_GITLAB_API_URL override them.
A committed auth.writeback in .keryx.yaml does nothing in CI: GTB ignores the
auth.* subtree of a project-local file until the file is trusted, and a pipeline
runs untrusted by design, so a cloned repository cannot redirect where a credential
is written. keryx's environment layer (KERYX_ prefix, . becomes _) sits above
the project file and is never trust-filtered.
The backend updates the same masked, protected variables the adapters read, keyed by
each token's variable name (e.g. TIKTOK_REFRESH_TOKEN). It only ever updates an
existing variable's value — it never creates a new, unmasked secret.
3. Set the CI/CD variables¶
In the owning project → Settings → CI/CD → Variables, all Masked and Protected:
| Variable | Why | Written back? |
|---|---|---|
TIKTOK_REFRESH_TOKEN |
TikTok's rotating refresh token | yes — every run |
INSTAGRAM_ACCESS_TOKEN |
Instagram long-lived token | yes, near expiry |
YOUTUBE_REFRESH_TOKEN |
YouTube durable refresh token | no (health-check only) |
LINKEDIN_ACCESS_TOKEN |
LinkedIn access token | no (standard app can't refresh) |
LINKEDIN_REFRESH_TOKEN |
only if you're a LinkedIn MDP partner | yes (MDP only) |
GITLAB_TOKEN |
api-scoped project/group access token for write-back | — |
GITLAB_TOKENmust have theapiscope —CI_JOB_TOKENcannot write CI/CD variables. Make it a project or group access token and mark it Protected.CI_PROJECT_IDandCI_API_V4_URLare predefined by GitLab; you don't set those.
Plus each platform's existing client secrets/ids (TIKTOK_CLIENT_SECRET,
YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET, INSTAGRAM_APP_SECRET /
INSTAGRAM_USER_ID, LINKEDIN_CLIENT_SECRET) as set up in the credential guides.
4. Configure alerts¶
Any failure (or an impending non-refreshable expiry) must reach a human. The red pipeline is the always-on baseline; add a notifier for an active ping:
token-refresh:
variables:
# one or several, comma-separated: webhook | email | discord | gitlab-issue
KERYX_AUTH_ALERTS_BACKEND: "discord,gitlab-issue"
KERYX_AUTH_ALERTS_GITLAB_PROJECT: phpboyscout/org # where the issue is opened
# ALERT_DISCORD_WEBHOOK_URL: a masked variable (the channel's incoming webhook)
# GITLAB_TOKEN: api-scoped, for the issue (and the write-back)
# webhook: ALERT_WEBHOOK_URL (Slack/Teams shape); email: KERYX_AUTH_ALERTS_EMAIL_HOST,
# _PORT (587), _FROM, and ALERT_SMTP_USERNAME / ALERT_SMTP_PASSWORD
gitlab-issue opens one issue per platform (keryx: linkedin needs attention,
labelled keryx-alert and ready-for-human) and comments on it while the condition
holds, so a token that keeps alerting is one ticket with a history. Close it once the
token is rotated. Every alert also carries the pipeline URL when it ran in CI.
The same auth.* rule as the write-back applies: these are job variables, not
committed keys. Locally (a trusted workstation) the keys live in ~/.keryx/config.yaml
under auth.alerts as the reference
lists them.
5. Add the scheduled job¶
In the owning project's .gitlab-ci.yml:
token-refresh:
stage: maintenance
image: registry.gitlab.com/phpboyscout/keryx:latest # or however you install keryx
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TASK == "token-refresh"'
variables:
KERYX_AUTH_WRITEBACK_BACKEND: gitlab
script:
- keryx auth refresh all --ci
# GITLAB_TOKEN, TIKTOK_REFRESH_TOKEN, … come from the protected CI/CD variables above.
If some enabled platforms only ever post from your own machine, so CI holds no
credentials for them, name the platforms CI does serve instead of all:
keryx auth refresh bluesky facebook linkedin --ci. Otherwise those platforms
alert on every run, and a job that is always red hides the alert that matters.
The announcement sweep that runs beside this is in Announce articles from a scheduled pipeline.
Then CI/CD → Schedules → New schedule:
- Interval: weekly (see cadence below).
- Variable:
SCHEDULE_TASK=token-refresh(so this schedule runs only this job). - Target the protected default branch (the variables are Protected).
A failing run turns the schedule red and fires your notifier; a green run means every enabled platform is fresh (and TikTok's rotated token has been written back for next time).
Cadence — why weekly¶
Weekly sits comfortably inside every window: it keeps Instagram's ~60-day token fresh, rotates TikTok well within its ~365-day refresh wall, health-checks YouTube's durable token, and gives ~7 weeks of warning before a LinkedIn token expires (the alert fires at 14 days). More frequent is harmless; much less frequent risks missing the LinkedIn re-auth window.
When an alert fires¶
- Instagram / TikTok / YouTube "refresh failed" — the refresh/rotate/health-check
didn't work; the token is likely revoked or dead. Re-capture it:
keryx auth <platform>, then update the CI variable. - LinkedIn "re-auth due" — a standard LinkedIn app can't refresh; run
keryx auth linkedinbefore the date in the alert and updateLINKEDIN_ACCESS_TOKEN.
See also¶
- Schedule unattended posting — the
publish dueside of the same scheduled-pipeline story. - Spec
0010-auth-refresh.md— the design, the per-platform token shapes, and the pluggable write-back/notifier backends.