CI workflows

GitHub Actions workflows that automate PR checks, label management, and other CI/CD processes.

All workflow files live under .github/workflows/.

PR approval labels

The following workflows work together to automatically manage approval-related labels on pull requests:

Workflow fileTriggerPrivileges
pr-review-trigger.ymlpull_request_reviewMinimal (no secrets)
pr-approval-labels.ymlpull_request_target, workflow_runApp token for label edits and org/team reads
blog-publish-labels.ymlschedule (daily 7 AM UTC)App token + SLACK_WEBHOOK_URL secret

Labels managed

  • missing:docs-approval — added when approval from the docs-approvers team is pending; removed once a docs-approver approves.
  • missing:sig-approval — added when approval from a SIG team is pending (determined by files changed and .github/component-owners.yml); removed once a SIG member approves or when no SIG component is touched.
  • ready-to-be-merged — added when all required approvals are present; removed otherwise. For PRs carrying any label in PUBLISH_DATE_LABELS (currently: blog), this label is also gated on the publish date found in changed files.

Publish date gating

The script scans each changed file for a line beginning with date: (typically from the front matter in Markdown content). If it finds a date in the future, the ready-to-be-merged label is withheld until that date arrives (UTC). This helps prevent content from being merged before its scheduled publication date.

The check applies to PRs carrying any label listed in the PUBLISH_DATE_LABELS environment variable, set in each workflow YAML (currently: blog). Adding a label extends the check to other PR types.

If a PR contains multiple files with different dates, the label is gated on the latest date — all content must be ready before merging.

Script operating modes

The pr-approval-labels.sh script processes a single PR (set via the PR environment variable). It is called by pr-approval-labels.yml on PR events and by blog-publish-check.sh in batch mode.

The blog-publish-check.sh script handles batch iteration: it queries all open PRs carrying any PUBLISH_DATE_LABELS label and calls pr-approval-labels.sh for each one. Used by the blog-publish-labels.yml schedule trigger (daily at 7 AM UTC), so a PR whose publish date arrives overnight receives ready-to-be-merged automatically without requiring a new commit.

Why two workflows?

GitHub’s pull_request_review event has no _target variant. This means a workflow triggered by a review on a fork PR runs in the fork’s context and cannot access the base repository’s secrets.

To work around this limitation, the system uses a workflow_run chaining pattern:

  1. pr-review-trigger runs on every review submission/dismissal. It saves the PR number as an artifact and exits — no secrets needed.
  2. pr-approval-labels is triggered by workflow_run (when the trigger workflow completes). It runs in the base repository context with full access to the GitHub App token, downloads the artifact, and updates labels.

For content changes (opened, reopened, synchronize), the pr-approval-labels workflow is triggered directly via pull_request_target.

sequenceDiagram
    participant R as Reviewer
    participant GH as GitHub
    participant T as pr-review-trigger
    participant L as pr-approval-labels

    R->>GH: Submits review (approve/request changes/dismiss)

    Note over GH: pull_request_review event

    GH->>T: Trigger (fork context, no secrets)
    T->>T: Save PR number as artifact
    T->>GH: Upload artifact, workflow completes

    Note over GH: workflow_run event (completed)

    GH->>L: Trigger (base repository context, with secrets)
    L->>L: Download PR number artifact
    L->>L: Run pr-approval-labels.sh
    L->>GH: Add/remove labels
sequenceDiagram
    participant A as Author
    participant GH as GitHub
    participant L as pr-approval-labels

    A->>GH: Opens/updates PR

    Note over GH: pull_request_target event

    GH->>L: Trigger directly (base repo context, with secrets)
    L->>L: Run pr-approval-labels.sh
    L->>GH: Add/remove labels

Security model

  • pr-review-trigger: intentionally minimal — no secrets, no privileged permissions. Ignores review.state == "commented" since comments don’t affect approvals.
  • pr-approval-labels: runs with a GitHub App token (OTELBOT_DOCS_APP_ID / OTELBOT_DOCS_PRIVATE_KEY) that has permissions to read org/team membership and edit PR labels. Uses pull_request_target and workflow_run to ensure it always executes in the trusted base repository context.
  • blog-publish-labels: runs on a schedule with a GitHub App token and the SLACK_WEBHOOK_URL secret. Always executes in the trusted base repository context (schedule events have no fork variant).

Blog publish labels

The blog-publish-labels.yml workflow runs daily at 7 AM UTC. It executes blog-publish-check.sh, which iterates over all open PRs with blog label and calls pr-approval-labels.sh for each one. When ready-to-be-merged is newly applied to any of them, a Slack notification is posted. You can also trigger it manually via workflow_dispatch with the force_notify input to send a test Slack notification. When force_notify is true, the labeling step is skipped entirely (dry run) — only the test Slack payload is sent.

Workflow fileTriggerSecrets required
blog-publish-labels.ymlschedule (daily 7 AM UTC), workflow_dispatch (manual test via force_notify)OTELBOT_DOCS_PRIVATE_KEY, SLACK_WEBHOOK_URL

The Slack notification fires only when the label transitions from absent to present on that run — repeated daily runs for an already-labeled PR do not re-notify. When triggering the workflow manually, set force_notify to true to send a one-off test notification (no labels are applied) so you can verify the Slack formatting.

Slack webhook setup

The workflow uses a Slack Workflow Builder webhook trigger, which allows non-engineers to own the message format without touching workflow code.

Create the webhook:

  1. In Slack: Tools → Workflow Builder → New Workflow → Start from scratch

  2. Choose trigger: Webhook

  3. Declare one variable — name: pr_list, type: Text

  4. Add a step: Send a message to the desired channel, with body:

    :newspaper: *Blog posts ready to publish*
    
    The following PRs have reached their publish date and all required
    approvals — they are ready to be merged:
    
    {{pr_list}}
    
    Have a great day! :sunny:
    

    Then click Add button and configure:

    • Label: Review and merge
    • Color: Primary (green)
    • Action: Open a link
    • URL: https://github.com/open-telemetry/opentelemetry.io/issues?q=is%3Apr+state%3Aopen+label%3Ablog+label%3Aready-to-be-merged
  5. Publish the workflow and copy the webhook URL

  6. Add it to the repository: Settings → Secrets and variables → Actions → New repository secret, name: SLACK_WEBHOOK_URL

Payload sent by the workflow:

{
  "pr_list": "• #123: Add blog post: OTel 1.0 — https://github.com/.../pull/123\n• #456: Announce: new SIG — https://github.com/.../pull/456"
}

Each PR is a bulleted line with its title and URL. Slack auto-links bare URLs. Multiple PRs labeled on the same day are batched into a single message — one webhook call regardless of how many PRs are ready.

sequenceDiagram
    participant GH as GitHub
    participant W as blog-publish-labels
    participant B as blog-publish-check.sh
    participant L as pr-approval-labels.sh
    participant S as Slack

    Note over GH: schedule event (daily, 7 AM UTC)

    GH->>W: Trigger (base repository context, with secrets)
    W->>B: Run blog-publish-check.sh
    B->>GH: Query open PRs with PUBLISH_DATE_LABELS labels
    GH-->>B: List of PRs
    loop Each PR
        B->>L: Run pr-approval-labels.sh (PR=number)
        L->>GH: Add/remove labels
    end
    alt Any PR newly labeled ready-to-be-merged
        W->>S: POST Slack notification with PR links
    end

PR fix directives

The pr-actions.yml workflow lets contributors run selected fix scripts by commenting on a PR:

  • /fix runs npm run fix.
  • /fix:<name> runs npm run fix:<name> (for example, /fix:format).
  • /fix:all is mapped to /fix since the command semantics changed (#9291).
  • /fix:ALL is mapped to fix:all so that maintainers can run fix:all.

The directive must be the first line of the comment; any following lines are ignored, so you can add an explanation after it. The workflow itself triggers on any comment whose body starts with /fix (so for example /fixup enters the pipeline and gets invalid-directive feedback, while a comment starting with a space, or with /fix only on a later line, does not trigger the workflow at all).

It runs as a four-stage pipeline:

  1. ack (trusted): as soon as a directive is received, replies with a 🔄 in-progress comment that links to the directive comment and to the run.
  2. generate-patch (untrusted): checks out the PR branch, runs the fix command, prunes the link refcache, and uploads a patch artifact (site.patch), up to 1024 KB.
  3. apply-patch (trusted): calls the reusable-apply-patch.yml workflow — resolved from the default branch, never from the PR — which applies the patch with a GitHub App token and pushes a commit to the PR branch. Skipped when the command produced no changes.
  4. report (trusted): replaces the acknowledgement with the final outcome when possible, or posts a new outcome comment when no acknowledgement exists, such as for closed PRs. Each directive thus normally maps to a single comment that links back to the directive and to the run that produced it. This covers every directive that triggers the workflow, including invalid directives (such as /fixup or /fix please), no-op runs, and failures that happen before any patch is produced.

Directives only run against open PRs (draft PRs included): on a closed or merged PR the fix command never runs and the report job explains why. The PR state comes from the trigger payload, so no runner is spent on the fix itself.

The pipeline only runs in the canonical open-telemetry repository, where the bot app credentials exist. Fork PRs work normally — issue_comment events fire in the base repository — but the workflow skips itself inside forks.

Directives follow latest-wins semantics: a new /fix comment on a PR cancels that PR’s in-flight run (which still reports a ⚠️ outcome), since concurrent fix runs on the same branch serve no purpose — the second push would fail anyway once the branch has moved.

The directive parser lives in scripts/gh/pr-fix/, patch generation is the npm-script-patch action, and the acknowledgement and outcome comments are composed by scripts/gh/patch-report/; all are unit tested via npm run test:local-tools.

Housekeeping

The housekeeping.yml workflow runs an approved fix command — fix-and-test:all by default, or an npm script given via manual (maintainer-only) dispatch — daily at 21:37 UTC, about 12 hours after the other daily automation jobs, and publishes any resulting changes as a PR. It is the second caller of the reusable patch actions, and the scheduled-maintenance flow that motivated #6592.

It runs as a three-stage pipeline:

  1. generate-patch: runs the housekeeping command via the npm-script-patch action and uploads the changes as a patch artifact. Unlike the /fix pipeline, the whole run is trusted: the schedule and dispatch triggers only ever execute default-branch code. A failing command fails the job, but any fixes it produced are still published, with a partial-results warning in the PR body.
  2. publish-patch: calls the reusable-patch-pr.yml workflow — the sibling of reusable-apply-patch.yml for callers without a PR context — which force-pushes the patch to the stable otelbot/housekeeping branch, recreated from main on every run, and opens a PR for it unless one is already open. There is thus at most one housekeeping PR at a time, always carrying the latest results. Any commits pushed to the branch — manual or via /fix — are clobbered by the next run, so merge the PR promptly if you push commits to it. Skipped when the command produced no changes, leaving any open housekeeping PR as is. Auto-merge is safe to enable on housekeeping PRs provided that stale approvals are dismissed when commits are pushed: required reviews then remain the control over the machine- and internet-derived content, even across force-pushes.
  3. report-failure: files a tracking issue on failure, via workflow failure reporting; when fixes were published, the issue links to the housekeeping PR.

Locale auto-merge

The locale-auto-merge.yml workflow lets a locale’s maintainers enable GitHub auto-merge on a locale-only PR through an /auto-merge (or /auto-merge:enable / /auto-merge:disable) comment directive — for placement rules, see the helper README. It runs as the DOCS bot, which holds the privileges needed to flip the “merge when ready” switch under branch protection; CODEOWNERS and required checks remain the hard merge gate.

The thin workflow delegates to the testable helper in scripts/gh/locale-auto-merge/, which enforces two guards before acting: every changed file must be locale-owned, and the commenter must be a member of the docs-<loc>-maintainers team for every locale the PR touches. The helper’s eligibility and authorization rules (and how to dry-run them locally) are documented in its README; its unit and integration tests run with npm run test:local-tools. Contributor-facing usage lives in the localization guide.

Spec integration branches

Two scheduled workflows track unreleased changes from upstream spec repositories and keep a draft PR (“integration branch”) current with the next development version:

Workflow fileUpstream repositoryBranch slug
update-spec-integration-branch.ymlopentelemetry-specificationspec
update-semconv-integration-branch.ymlsemantic-conventionssemconv

Both workflows delegate the “pick the next version + branch” step to a shared Node helper, scripts/gh/specs/pick-branch/cli.mjs. The helper:

  • Reuses an existing otelbot/<slug>-integration-vX.Y.Z-dev branch when one exists and the version has not yet been released; otherwise bumps the latest release tag’s minor version.
  • Writes VERSION and BRANCH to $GITHUB_ENV for downstream steps.
  • Opens a tracking issue (label <slug>-integration-warning, deduplicated) when it detects problems such as multiple stale integration branches.

Run modes

The helper auto-selects between dry-run and write mode and prints a [mode] banner explaining its choice:

ContextDefault behaviorOverride
GitHub Actionswritepass --dry-run
Local (anywhere else)dry-runpass --no-dry-run

Locally, dry-run still runs all read-only git/gh commands (so the issue deduplication check executes), but skips writes. With --no-dry-run the helper uses your local gh credentials; if GITHUB_ENV is unset, VERSION/BRANCH are printed to stdout only. Try it:

node scripts/gh/specs/pick-branch/cli.mjs --spec=otel
node scripts/gh/specs/pick-branch/cli.mjs --spec=semconv --no-dry-run
node scripts/gh/specs/pick-branch/cli.mjs --help

Pure logic and CLI argument parsing live in index.mjs and are covered by *.test.mjs files in the same folder (npm run test:local-tools to run them).

Workflow failure reporting

reusable-report-failure.yml opens (or comments on) a tracking issue when a caller workflow fails. How to wire it up, optional inputs, and caller context behavior are documented in the workflow file header; issue logic lives in scripts/gh/report-failure/ (npm run test:local-tools).

Other workflows

The repository includes several other workflows:

WorkflowPurpose
check-links.ymlSharded link checking using htmltest
check-text.ymlTextlint terminology checks
check-i18n.ymlLocalization front matter validation
check-spelling.ymlSpell checking
test.ymlTest (excludes test:base)
auto-update-registry.ymlAuto-update registry package versions
auto-update-versions.ymlAuto-update OTel component versions
build-dev.ymlDevelopment build and preview
lint-scripts.ymlShellCheck linting for .github/scripts/
label-manager.ymlPR labels (component labels & approval flow)
component-owners.ymlAssign reviewers based on component ownership