Skip to content

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
Instagram 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)
LinkedIn 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_TOKEN must have the api scope — CI_JOB_TOKEN cannot write CI/CD variables. Make it a project or group access token and mark it Protected. CI_PROJECT_ID and CI_API_V4_URL are 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 linkedin before the date in the alert and update LINKEDIN_ACCESS_TOKEN.

See also

  • Schedule unattended posting — the publish due side 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.