Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/cli-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3815,6 +3815,88 @@
}
]
},
{
"path": [
"changelog"
],
"name": "migrate-from-web",
"summary": "TEMPORARY: One-off migration of published release notes into the S3 bundle store; removed after elastic/docs-eng-team#683.",
"notes": "Fetches the release-notes Markdown that backs the published pages (at the pinned git ref in the\nchecked-in scope table), maps it to the existing bundle YAML shape, and uploads to\nbundle/{product}/ with create-only semantics (If-None-Match: *) \u2014 existing keys are\nskipped, never overwritten. Prints a per-key run report (created / skipped / failed with reason and\nobject ETag) suitable for pasting into the tracking issue.\n\nMigrates every product in the checked-in scope table by default. The table lives in code\n(MigrateFromWebScope.All: product id \u2192 source repo, release-notes path, pinned ref, version\ncutoff) and grows per rollout wave; use --products to narrow a run for tests and pilots.\nTracked by elastic/docs-eng-team#736.",
"usage": "docs-builder changelog migrate-from-web [options]",
"examples": [],
"parameters": [
{
"role": "flag",
"name": "products",
"type": "array",
"required": false,
"summary": "Optional: restrict the run to specific product ids (comma-separated or repeated), e.g. \u0022edot-java\u0022. Defaults to every product in the checked-in scope table.",
"repeatable": true,
"elementType": "string"
},
{
"role": "flag",
"name": "s3-bucket-name",
"type": "string",
"required": false,
"summary": "Destination S3 bucket. Required unless --dry-run; when provided with --dry-run, existing keys are still inspected so the report distinguishes would-create from skipped."
},
{
"role": "flag",
"name": "versions",
"type": "array",
"required": false,
"summary": "Optional: restrict the run to specific versions (comma-separated or repeated). Versions above a product\u0027s cutoff are always skipped.",
"repeatable": true,
"elementType": "string"
},
{
"role": "dryRun",
"name": "dry-run",
"type": "boolean",
"required": false,
"summary": "Do everything except the S3 writes and report what would be created.",
"defaultValue": "false"
},
{
"role": "flag",
"name": "log-level",
"shortName": "l",
"type": "enum",
"required": false,
"summary": "Minimum log level. Default: information",
"enumValues": [
"trace",
"debug",
"information",
"warning",
"error",
"critical",
"none"
]
},
{
"role": "flag",
"name": "config-source",
"shortName": "c",
"type": "enum",
"required": false,
"summary": "Override the configuration source: local, remote",
"enumValues": [
"local",
"remote",
"embedded"
]
},
{
"role": "flag",
"name": "skip-private-repositories",
"type": "boolean",
"required": false,
"summary": "Skip cloning private repositories"
}
]
},
{
"path": [
"changelog"
Expand Down
92 changes: 92 additions & 0 deletions docs/cli/changelog/cmd-migrate-from-web.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
## Description

:::{warning}
This command is **temporary**. It exists solely to migrate release notes that were published before the changelog pipeline existed into the S3 bundle store, and it will be deleted once the migration rollout ([docs-eng-team#683](https://github.com/elastic/docs-eng-team/issues/683)) completes. Do not build workflows on top of it.
:::

One-off migration of already-published release notes into the S3 bundle store. For each product in scope, the command:

1. Fetches the release-notes Markdown that backs the published pages — from `raw.githubusercontent.com` at the pinned commit recorded in the scope table, not by scraping live site HTML.
2. Parses each `## {version}` section (typed `### …` subsections become entries; prose is preserved as the bundle description) and maps it to the **existing** bundle YAML shape that [](/cli/changelog/upload.md) publishes. No new schema is introduced.
3. Uploads each release to `bundle/{product}/{version}.yaml` with **create-only** semantics (`If-None-Match: *`): keys that already exist are skipped and never overwritten, so the migration can never clobber bundles produced by the live pipeline.
4. Prints a per-key run report (created / skipped / failed, with the reason and object ETag) suitable for pasting into the tracking issue.

By default the command migrates **every product in the checked-in scope table**; use `--products` to narrow a run for tests and pilots.

## Migration scope

The scope table is checked into the command itself (`MigrateFromWebScope.All` in the docs-builder repository) rather than into a config file — it is temporary tooling state, added per rollout wave and deleted with the command. Each entry maps a product id (the `bundle/{product}/` S3 prefix, see `config/products.yml`) to the source of its published release notes and a version cutoff:

| Field | Meaning |
| ----- | ------- |
| `Owner` / `Repo` | GitHub repository whose docs back the published release notes. |
| `Path` | Repo-relative path of the release-notes Markdown page. |
| `Ref` | Pinned commit SHA at which the Markdown is fetched (reproducible runs). |
| `Cutoff` | Inclusive upper version bound; releases above it belong to the live pipeline. |

The page→product mapping is deliberately explicit: bundle product ids appear in no published metadata (page frontmatter carries the site taxonomy, not bundle ids), so deriving it automatically is not possible. Adding a product to the migration is a small PR against the table.

Releases above a product's cutoff are always skipped — they are owned by the live changelog pipeline. Use `--versions` to narrow a run to specific versions below the cutoff.

## Requirements

Uploads use the same AWS SDK credential chain, region, and IAM permissions as [](/cli/changelog/upload.md). No credentials are needed for `--dry-run` without `--s3-bucket-name`.

## Run report

The report lists one line per key with its outcome:

| Outcome | Meaning |
| ------- | ------- |
| `created` | The key did not exist and was written (conditional PUT succeeded). |
| `would-create` | Dry run only: the key would be written. |
| `skipped` | The key already exists (identical or different content — never overwritten), was created concurrently by another writer, is beyond the cutoff, or is not in the `--versions` selection. |
| `failed` | The write failed; the reason is included and the command exits non-zero. |

The command writes YAML bundle objects only — never a `registry.json`. The scrubber Lambda owns the public `bundle/{product}/registry.json` manifests and the shallow per-tree maps, reconciling them from the S3 events these creates emit ([#3738](https://github.com/elastic/docs-builder/pull/3738), [#3760](https://github.com/elastic/docs-builder/pull/3760)).

## Examples

### Dry run without credentials

Parse, map, and report what would be created — no S3 access at all:

```sh
docs-builder changelog migrate-from-web --dry-run
```

### Dry run against the real bucket

Also checks which keys already exist, so the report distinguishes `would-create` from `skipped`:

```sh
docs-builder changelog migrate-from-web \
--dry-run \
--s3-bucket-name my-changelog-bundles
```

### Perform the migration

```sh
docs-builder changelog migrate-from-web \
--s3-bucket-name my-changelog-bundles
```

Re-running the same command is safe: every existing key is reported as `skipped` and the run is a no-op.

### Migrate a single product (pilots and tests)

```sh
docs-builder changelog migrate-from-web \
--products edot-java \
--s3-bucket-name my-changelog-bundles
```

### Migrate specific versions only

```sh
docs-builder changelog migrate-from-web \
--products edot-java \
--s3-bucket-name my-changelog-bundles \
--versions 1.9.0,1.10.0
```
13 changes: 7 additions & 6 deletions src/Elastic.Documentation.Tooling/FileSystemFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,11 @@ public static ScopedFileSystem ScopeCurrentWorkingDirectory(IFileSystem inner, I
// Builds write options that include AllowedSpecialFolders.Temp PLUS the inner FS's own
// GetTempPath() as an explicit root — but only when the inner FS is MockFileSystem.
//
// On non-Windows MockFileSystem hardcodes a Unix-ified path ("/temp/", derived from "C:\temp")
// instead of calling System.IO.Path.GetTempPath(). AllowedSpecialFolder.Temp uses the real
// GetTempPath() (e.g. "/tmp/" on Linux), so the two diverge and scope validation fails for any
// path created via mockFs.Path.GetTempPath().
// MockFileSystem hardcodes its temp path ("C:\temp" on Windows, unix-ified to "/temp/"
// elsewhere) instead of calling System.IO.Path.GetTempPath(). AllowedSpecialFolder.Temp uses
// the real GetTempPath() (e.g. "/tmp/" on Linux, "C:\Users\<user>\AppData\Local\Temp" on
// Windows), so the two diverge on every OS and scope validation fails for any path created
// via mockFs.Path.GetTempPath().
//
// Fix tracked upstream: https://github.com/TestableIO/System.IO.Abstractions/pull/1454
// Once that ships and we update the package reference we can drop this workaround.
Expand All @@ -153,9 +154,9 @@ private static ScopedFileSystemOptions BuildWriteOptions(IFileSystem inner, para
{
var allRoots = roots.ToList();
var innerType = inner is ScopedFileSystem sf ? sf.InnerType : inner.GetType();
if (!OperatingSystem.IsWindows() && innerType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
if (innerType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
{
// Cover MockFileSystem's unixified hardcoded temp path
// Cover MockFileSystem's hardcoded temp path
var innerTemp = inner.Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (!string.IsNullOrEmpty(innerTemp) && !allRoots.Contains(innerTemp, StringComparer.OrdinalIgnoreCase))
allRoots.Add(innerTemp);
Expand Down
87 changes: 87 additions & 0 deletions src/services/Elastic.Changelog/Migration/MigrateFromWebScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Immutable;
using Elastic.Documentation.Diagnostics;

namespace Elastic.Changelog.Migration;

/// <summary>
/// TEMPORARY (elastic/docs-eng-team#736): one product's migration scope — where its published
/// release-notes Markdown lives (owner/repo/path at a pinned ref) and the inclusive version cutoff.
/// The checked-in table below replaces the former <c>config/migrate-from-web.yml</c> (dropped on
/// review: no standing config surface for a one-off tool). It grows per rollout wave
/// (elastic/docs-eng-team#683) and is deleted together with the command once the rollout completes.
/// </summary>
public sealed record MigrateFromWebScope
{
public required string ProductId { get; init; }

/// <summary>GitHub owner of the source repository (e.g. <c>elastic</c>).</summary>
public required string Owner { get; init; }

/// <summary>Source repository name (e.g. <c>elastic-otel-java</c>).</summary>
public required string Repo { get; init; }

/// <summary>Repository-relative path of the release-notes Markdown page.</summary>
public required string Path { get; init; }

/// <summary>Pinned git ref (commit SHA) at which the Markdown is fetched (reproducible runs).</summary>
public required string Ref { get; init; }

/// <summary>Inclusive upper version bound; releases above it belong to the live pipeline.</summary>
public required string Cutoff { get; init; }

/// <summary>
/// Every product the migration knows how to source. The page→product mapping is deliberately
/// checked in rather than derived: bundle product ids appear in no published metadata (page
/// frontmatter carries the site taxonomy, not bundle ids), so each entry pins its source
/// explicitly. A run covers the whole table unless narrowed with <c>--products</c>.
/// </summary>
public static ImmutableArray<MigrateFromWebScope> All { get; } =
[
new()
{
ProductId = "edot-java",
Owner = "elastic",
Repo = "elastic-otel-java",
Path = "docs/release-notes/index.md",
// Last commit before the repo switched to native docs-builder bundle YAMLs (#1023):
// the final hand-authored state of the published release-notes Markdown.
Ref = "9a61ce4faaf08e272c433a083bcc6f0e96d80e0a",
Cutoff = "1.10.0"
}
];

/// <summary>
/// Resolves a <c>--products</c> selection against the table — the whole table when the selection
/// is empty — or null (with an error emitted) when any requested id is unknown.
/// </summary>
public static IReadOnlyList<MigrateFromWebScope>? Select(IDiagnosticsCollector collector, IReadOnlyList<string> products)
{
if (products.Count == 0)
return All;

var byId = All.ToDictionary(s => s.ProductId, StringComparer.Ordinal);
var selected = new List<MigrateFromWebScope>(products.Count);
var unknown = new List<string>();
foreach (var product in products.Distinct(StringComparer.Ordinal))
{
if (byId.TryGetValue(product, out var scope))
selected.Add(scope);
else
unknown.Add(product);
}

if (unknown.Count > 0)
{
var known = string.Join(", ", All.Select(s => s.ProductId).Order(StringComparer.Ordinal));
collector.EmitError(string.Empty,
$"Unknown product id(s) in --products: {string.Join(", ", unknown)}. Products in the checked-in migration scope: {known}. Add an entry to MigrateFromWebScope.All before running the migration.");
return null;
}

return selected;
}
}
Loading
Loading