Skip to content

ci: split CD into GitHub release + Azure publish stages, with crate support#7531

Merged
janechu merged 20 commits into
mainfrom
users/janechu/update-cd-pipeline
May 26, 2026
Merged

ci: split CD into GitHub release + Azure publish stages, with crate support#7531
janechu merged 20 commits into
mainfrom
users/janechu/update-cd-pipeline

Conversation

@janechu
Copy link
Copy Markdown
Collaborator

@janechu janechu commented May 22, 2026

Pull Request

📖 Description

Split the single nightly Azure CD job into two coordinated jobs so that npm credentials never leave the Azure environment. GitHub Actions packages each non-private workspace into an auditable GitHub release; the existing Azure release template publishes those tarballs to npm and crates.io. GitHub Releases are the source of truth, and deployed/<tag> git marker tags track which releases have already been published.

GitHub Actions side (cd-github-releases.yml)

  • Runs on a nightly cron (0 8 * * * UTC, ~12am PST) plus workflow_dispatch. Does not bump versions or push source changes — version bumps land via ordinary human-authored PRs (e.g., from npm run bump). The cron is scheduled ~1 hour before the Azure CD pipeline (09:00 UTC) so any releases this job creates are picked up by that same night's publish run.
  • Two jobs:
    1. detect runs create-github-releases.mjs --check-only. The script walks the workspaces tree (no npm ci required), computes ${name}_v${version} for each non-private workspace, and emits hasMissingReleases=true if any of those git tags do not yet exist (uses git rev-parse refs/tags/<tag> — no API call).
    2. release runs only when missing releases exist. Installs Node, the Rust toolchain (for cargo package), builds, then for every missing release: packs the npm tarball into publish_artifacts_npm/, packs the paired Rust crate (where crates/<crate-name>/Cargo.toml exists — derived by dropping @ and replacing / with - in the npm name) into publish_artifacts_crates/, and creates the GitHub release with both assets attached via gh release create --target <sha>. The gh CLI creates a lightweight ${name}_v${version} tag atomically with the release, so the release and its tag exist iff each other does — eliminating the orphan-tag risk that an explicit pre-gh tag-push would have.

Azure Pipelines side (azure-pipelines-cd.yml)

  • Cron 0 9 * * * (09:00 UTC daily; ~1am Pacific in standard time, ~2am during DST) with always: true so it still runs on no-op nights — work depends on external GitHub state, not repo commits.
  • Two stages:
    1. Check runs download-github-releases.mjs --check-only. The script lists every git tag matching the beachball-style ${name}_v${version} pattern that does not also have a deployed/<tag> counterpart and emits the list via Azure Pipelines setvariable output variables (needsDeployment, undeployedTags). No network calls to npm.org or crates.io.
    2. Package depends on Check and runs only when needsDeployment == 'true'. Downloads every undeployed release's assets via gh release download, sorts them into publish_artifacts_npm/ (.tgz) and publish_artifacts_crates/ (.crate), hands off to FAST.Release.PipelineTemplate.yml@fastPipelines, and on success pushes a deployed/<tag> marker tag for each release that was just published. The next nightly run sees those markers and skips the corresponding releases.

Paired npm package / Rust crate versioning

  • @microsoft/fast-build is paired with the microsoft-fast-build Rust crate at crates/microsoft-fast-build/. This PR bumps that crate from 0.1.0 → 0.7.0 to match the existing npm version.
  • A new postbump hook in beachball.config.js automatically rewrites the crate's Cargo.toml and Cargo.lock whenever npm run bump bumps the paired npm package, so PR authors don't have to remember to sync them by hand. The crate name is derived from the npm name by dropping @ and replacing / with -; the hook is a no-op for npm packages without a paired crate.
  • create-github-releases.mjs still errors out at release time if the two versions disagree, as a safety net.

Other changes

👩‍💻 Reviewer Notes

  • Idempotency is enforced entirely through git tags. ${name}_v${version} on the GitHub side, deployed/${name}_v${version} on the Azure side. Neither side needs to talk to npm.org or crates.io to decide whether work is required.
  • Release tag format matches beachball's existing ${name}_v${version} (see node_modules/beachball/lib/git/generateTag.js), so the git tag, the GitHub release tag, and the eventual npm publish all share a single identifier.
  • The GitHub workflow needs the existing secrets.GH_TOKEN (same secret already used by cd-gh-pages.yml). No new secrets are required on either side.
  • Open question on FAST.Release.PipelineTemplate.yml@fastPipelines: the existing template may expect a single publish_artifacts/ folder rather than the separate publish_artifacts_npm/ + publish_artifacts_crates/ layout. If that's the case, either the template or this PR needs a follow-up tweak to bridge the layout difference.

📑 Test Plan

This change affects scheduled CD only, so it cannot be exercised end-to-end from a PR build. Verified locally:

  • npm run format:check and npm run checkchange pass.
  • node --check passes on both new scripts and beachball.config.js; npx @biomejs/biome check passes too.
  • js-yaml parses both .github/workflows/cd-github-releases.yml and azure-pipelines-cd.yml without errors.
  • Smoke run of create-github-releases.mjs --check-only against this repo correctly reports the 5 publishable workspaces (one with a paired crate) and hasMissingReleases=true.
  • Smoke run of download-github-releases.mjs --check-only against the live microsoft/fast repo correctly returns 0 undeployed tags. (After cleaning up some test releases that were created during development, the five publishable workspaces' tags now exist on origin and each one has a deployed/<tag> marker, so the script correctly reports nothing to publish.)
  • End-to-end beachball bump test: created a patch change file for @microsoft/fast-build, ran npx beachball bump, confirmed the postbump hook synced both crates/microsoft-fast-build/Cargo.toml and the matching Cargo.lock entry from 0.7.0 to 0.7.1 in the same bump commit.
  • cargo package --no-verify --allow-dirty --manifest-path crates/microsoft-fast-build/Cargo.toml produces the expected microsoft-fast-build-0.7.0.crate artifact.
  • workflow_dispatch is enabled on the new workflow so it can be triggered manually for the first end-to-end run after merge.

✅ Checklist

General

  • I have included a change request file using $ npm run change
  • I have added tests for my changes.
  • I have tested my changes.
  • I have updated the project documentation to reflect my changes.
  • I have read the CONTRIBUTING documentation and followed the standards for this project.

Note on the first checkbox: only .github/, build/scripts/, azure-pipelines-cd.yml, beachball.config.js, and crates/microsoft-fast-build/ are modified. .github/ is ignored by beachball.config.js; build/ is a private workspace; azure-pipelines-cd.yml and beachball.config.js are not part of any published workspace; and the crate version bump pairs with the existing @microsoft/fast-build 0.7.0 release. npm run checkchange confirms no change file is required.

⏭ Next Steps

  • After this merges, manually trigger cd-github-releases.yml via workflow_dispatch on a quiet day (no pending bumps) to validate the path end-to-end. Then trigger it on a day with pending changes to validate release creation. The Azure pipeline will pick up the resulting GitHub release the following night and the deployed/<tag> marker should appear immediately after the publish. From there the nightly cron will keep the cycle going automatically.
  • Verify whether FAST.Release.PipelineTemplate.yml@fastPipelines is happy with the separate publish_artifacts_npm/ and publish_artifacts_crates/ folders, or whether a small template tweak is needed (follow-up PR if so).
  • If desired in a follow-up: add a separate GitHub Actions workflow that emits a Teams/Discord notification when a release is created and again when the Azure publish completes, for easier observability.

@janechu janechu changed the title ci: split CD pipeline into GitHub release + npm publish stages ci: split CD into GitHub release + Azure publish stages, with crate support May 26, 2026
@janechu janechu requested a review from Copilot May 26, 2026 17:36
@janechu janechu marked this pull request as ready for review May 26, 2026 17:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the FAST monorepo’s nightly continuous deployment flow to split “release artifact creation” (GitHub Actions) from “publishing to registries” (Azure Pipelines), keeping npm credentials confined to Azure while using git tags + GitHub Releases as the idempotency/source-of-truth mechanism. It also adds paired Rust crate support (for @microsoft/fast-buildmicrosoft-fast-build) and a Beachball postbump hook to keep crate versions in sync with npm versions.

Changes:

  • Add a new GitHub Actions workflow (cd-github-releases.yml) + script to create per-package GitHub releases with packed .tgz (and optional .crate) assets.
  • Update Azure CD pipeline to detect undeployed releases, download assets from GitHub Releases, publish via the existing template, and push deployed/<tag> marker tags.
  • Add a Beachball postbump hook to automatically sync a paired crate’s Cargo.toml/Cargo.lock version with its npm package version.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
package.json Removes now-obsolete publish-ci script (Azure no longer uses Beachball for packing).
crates/microsoft-fast-build/Cargo.toml Bumps crate version to align with the existing npm package version.
crates/microsoft-fast-build/Cargo.lock Updates the lockfile entry to match the crate’s new version.
CONTRIBUTING.md Documents the maintainer bump-and-release workflow for the new CD design.
build/scripts/create-github-releases.mjs New script to detect missing ${name}_v${version} tags and create GitHub releases with .tgz/.crate assets.
build/scripts/download-github-releases.mjs New script to detect undeployed tags and download/sort GitHub release assets for Azure publishing.
beachball.config.js Adds a postbump hook to sync paired Rust crate versions during beachball bump.
azure-pipelines-cd.yml Splits CD into “Check” + “Package” stages; downloads GitHub release assets and tags successful deployments.
.gitignore Updates ignored artifact directories to match the new split npm/crate/meta/stage folders.
.github/workflows/README.md Documents the new two-part CD flow (GitHub release creation + Azure publish).
.github/workflows/cd-github-releases.yml New workflow that creates GitHub releases on push-to-main / manual dispatch.

Comment thread build/scripts/download-github-releases.mjs
Comment thread build/scripts/download-github-releases.mjs
Comment thread azure-pipelines-cd.yml
Comment thread .github/workflows/README.md Outdated
@janechu
Copy link
Copy Markdown
Collaborator Author

janechu commented May 26, 2026

Addressed all four review comments in bfbf8f6:

  • download-github-releases.mjs line 80 — Filter deployed/\* tags out before the release-tag regex test, and tighten the regex to require a full MAJOR.MINOR.PATCH (so prereleases like @microsoft/fast-html_v1.0.0-alpha.53 still match but accidental _v0.7 does not).
  • download-github-releases.mjs line 125publish_artifacts_stage/ is now cleared with rmSync({recursive: true, force: true}) before each iteration, and unknown-type files are deleted after warning so they cannot bleed into the next loop.
  • azure-pipelines-cd.yml line 138Mark releases as deployed step is now idempotent: a git rev-parse --verify --quiet refs/tags/deployed/<tag> check guards the tag/push so a re-run after partial success no longer aborts on the first pre-existing marker.
  • .github/workflows/README.md line 20 — Documentation now describes the atomic gh release create --target <sha> flow that the script actually implements (no more references to --verify-tag or to pushing an annotated tag separately).

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 6 comments.

Comment thread build/scripts/download-github-releases.mjs
Comment thread build/scripts/download-github-releases.mjs
Comment thread build/scripts/download-github-releases.mjs
Comment thread build/scripts/download-github-releases.mjs
Comment thread build/scripts/create-github-releases.mjs
Comment thread build/scripts/create-github-releases.mjs
@janechu
Copy link
Copy Markdown
Collaborator Author

janechu commented May 26, 2026

Addressed the six new review comments in aa73f47:

  • download-github-releases.mjs line 67 — Added a GH_TOKEN guard that fires immediately in non---check-only mode, so the workflow surfaces GH_TOKEN must be set so the \gh` CLI can download release assets.instead of a generic mid-loopghauth error.--check-only` still runs entirely off local git state and does not require a token.
  • download-github-releases.mjs line 116publish_artifacts_npm/, publish_artifacts_crates/, and publish_artifacts_meta/ are now rmSync({recursive:true, force:true})-cleared before being recreated (in addition to the per-iteration publish_artifacts_stage/ cleanup added in the previous commit) so a re-run after a partially-failed Azure attempt cannot pick up stale assets.
  • download-github-releases.mjs line 146 — Each iteration now counts recognised assets (.tgz/.crate); if zero are found the tag is not pushed to processed, an error is logged, and hasErrors is set. This prevents the Azure pipeline from pushing a deployed/<tag> marker for a release that never actually published anything.
  • download-github-releases.mjs line 150 + create-github-releases.mjs line 272catch (error) blocks now use const message = error instanceof Error ? error.message : String(error), so non-Error throws no longer log undefined or throw inside the catch handler.
  • create-github-releases.mjs line 177 — Added the same GH_TOKEN fast-fail check directly after the CHECK_ONLY early return, so creating releases also errors clearly when the token is unset.

Smoke-tested locally against microsoft/fast (which now has the first real release @microsoft/fast-test-harness_v0.3.0):

  • --check-only modes still run without a token
  • non-check mode without GH_TOKEN exits 1 immediately with the new error message
  • biome check / format:check / checkchange all clean

janechu and others added 15 commits May 26, 2026 13:32
Split the single nightly Azure CD job into two coordinated nightly jobs so
npm credentials never leave the Azure environment:

- .github/workflows/cd-github-releases.yml (new) runs at midnight PST.
  Bumps versions, packs tarballs, creates a GitHub release per package
  (tag format `${name}_v${version}`, matching beachball), and uploads
  each tarball as a release asset.
- azure-pipelines-cd.yml (modified) now runs at 1am PST. It downloads
  any GitHub release tarball that has not yet been published to npm into
  publish_artifacts/, then hands off to the existing
  FAST.Release.PipelineTemplate template for the actual npm publish.

Adds build/scripts/release-utils.mjs (shared helpers),
build/scripts/create-github-releases.mjs, and
build/scripts/download-github-releases.mjs. No new npm dependencies.

Updates .github/workflows/README.md to document the split.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per review feedback, the 600+ lines of custom Node.js GitHub REST client
(release-utils.mjs + two .mjs scripts) are overkill when the gh CLI is
pre-installed on every GHA runner and standard on Azure pool images.

Beachball already handles the bump / pack / commit / git-tag / push side of
the release flow; it just does not natively manage GitHub releases or
download-missing-from-npm flows. That bridging code is trivial when leaning
on existing tools:

- build/scripts/create-github-releases.sh: tar + jq read the embedded
  package.json from each tarball, then gh release create uploads it.
- build/scripts/download-github-releases.sh: gh release list iterates
  beachball-style tags, npm view skips already-published versions, and
  gh release download fetches the missing .tgz assets.

Updates both workflow YAMLs to call the .sh scripts and the workflows README
to point at them. Removes the .mjs scripts and release-utils.mjs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the bash helpers introduced in the previous commit with
equivalent Node.js (.mjs) scripts. The scripts remain thin wrappers
around the gh, npm, and tar CLIs — no extra npm dependencies or custom
GitHub API client. JavaScript matches the conventions of the existing
build/*.mjs helpers in the repo.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The nightly GitHub workflow no longer runs `npm run publish-ci`, so it
no longer bumps versions, creates git tags, commits, or pushes to main.
Version bumps now land on main through ordinary human-authored pull
requests (e.g. running `npm run bump` locally and opening a PR).

After such a PR is merged, the workflow re-packs every non-private
workspace at its current package.json version and creates a GitHub
release for any `${name}_v${version}` tag that does not already exist.
Re-runs are idempotent: existing releases are skipped.

This also removes the git user config, the elevated checkout token, and
the deep `fetch-depth: 0` from the workflow since the action no longer
writes to the repository.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Detect stage lists GitHub releases and, for each beachball-style
tag, checks whether that exact `${name}@${version}` is already on
npm. It emits a `hasReleases` output variable based on whether any
tarballs remain to be published.

The Publish stage depends on Detect and runs only when
`hasReleases == 'true'`. This means `npm ci`, the duplicate download,
and the FAST.Release.PipelineTemplate are skipped entirely on no-op
nights, which is now the common case once we are caught up.

The download is re-run inside Publish to bring the tarballs onto its
agent (stages run on separate VMs). The script is idempotent and the
tarballs are small, so the duplicate download is cheap compared to
shuttling them between agents as pipeline artifacts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror the patterns used by microsoft/webui's CD pipeline:

- GitHub workflow now triggers on push to main (plus workflow_dispatch),
  using `git rev-parse refs/tags/<tag>` for idempotency instead of a
  GitHub API call. Requires `fetch-depth: 0` to see all tags.
- Workflow now packs paired Rust crates (where
  `crates/<crate-name>/Cargo.toml` exists, derived by dropping `@` and
  replacing `/` with `-` in the npm name) into a separate
  `publish_artifacts_crates/` folder, alongside npm tarballs in
  `publish_artifacts_npm/`. Errors out if the crate version does not
  match the npm version.
- Bumps `microsoft-fast-build` crate to 0.7.0 to match the paired
  `@microsoft/fast-build` npm package.
- Azure pipeline now uses `deployed/<tag>` git marker tags (pushed
  after a successful publish) instead of `npm view`/`cargo search` to
  decide what to publish — avoids unreliable npm.org / crates.io calls
  from 1ES agents. Cron uses `always: true` so it still runs on no-op
  nights, since the work depends on external GitHub state rather than
  repo commits.
- Both stages of the Azure pipeline run with separate npm/crates
  publish folders so the existing FAST.Release.PipelineTemplate can be
  pointed at them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire a `postbump` hook into `beachball.config.js` so that whenever
`npm run bump` bumps an npm package that has a paired Rust crate
(e.g. `@microsoft/fast-build` -> `crates/microsoft-fast-build/`), the
crate's `Cargo.toml` and `Cargo.lock` entries get rewritten to the
new version in the same bump commit.

The crate name is derived from the npm name by dropping the leading
`@` and replacing `/` with `-`. The hook is a no-op for npm
packages without a paired crate.

This removes the manual step that authors of a version-bump PR for
`@microsoft/fast-build` would otherwise have to remember. The
version-mismatch check in `create-github-releases.mjs` remains as a
safety net in case someone hand-edits one file without the other.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ifact folders

Use `publish_artifacts_npm/` for beachball's local pack output (the
only artifacts beachball produces are npm tarballs) and update
.gitignore to cover the four publish folders the new CD scripts can
write to (npm, crates, meta, stage).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add `build/scripts/pack-crates.mjs` and chain it after beachball in the
`publish-ci` npm script so a single `npm run publish-ci` produces
both the npm tarballs (under `publish_artifacts_npm/`, via beachball's
`packToPath`) and the paired Rust `.crate` archives (under
`publish_artifacts_crates/`, via `cargo package`). Also drop a handful
of `.tgz` files in `publish_artifacts/` that were accidentally
committed when the old gitignore entry was renamed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Neither CD pipeline runs `npm run publish-ci` — the GitHub workflow
calls `npm pack` / `cargo package` directly from
`create-github-releases.mjs`. Drop:

- `build/scripts/pack-crates.mjs` (only consumer was publish-ci)
- `"publish-ci"` script in package.json
- `packToPath` in beachball.config.js (only read when packing via
  beachball, which nothing in the new flow does)

The four `publish_artifacts_*/` folders are still produced and
consumed at runtime by the CD scripts, so the .gitignore entries
stay.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… job

The release script previously created an annotated git tag and pushed
it before calling `gh release create`. If `gh release create` failed
(transient network blip, asset upload error, rate limit) after the
tag was already pushed, the tag would exist on the remote but no
release would exist for it. On the next workflow run,
`git rev-parse refs/tags/$tag` would return success and the script
would skip the release forever \u2014 stranded forever.

Let `gh release create` create the tag itself (via `--target <sha>`),
which is atomic with the release creation. Now "tag exists" and
"release exists" are always the same fact, and a failed release is
safely retried on the next workflow run.

The first manual publish run (zero existing tags, five packages)
benefits the most: any single transient failure no longer strands
that package's tag.

As a follow-on, drop the no-longer-needed git user config and the
`token:` argument to `actions/checkout`. `gh release create` uses
the `GH_TOKEN` env var, not the checkout token.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document the maintainer workflow for cutting a release:

1. Create a bump branch
2. Run `npm run bump` (which consumes change files, bumps
   package.json versions, runs the postbump hook to sync paired
   Cargo.toml + Cargo.lock, and deletes change files)
3. Review the diff and run
   `create-github-releases.mjs --check-only` to preview what CD
   will publish
4. Open and merge the bump PR
5. After merge, cd-github-releases.yml creates GitHub releases
   and the nightly Azure pipeline publishes to npm + crates.io

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- download-github-releases.mjs: tighten release tag regex to
  `_v\d+\.\d+\.\d+` (require a patch component) and explicitly
  exclude `deployed/<tag>` marker tags before applying it, so
  markers aren't re-treated as undeployed releases.
- download-github-releases.mjs: clear `publish_artifacts_stage/`
  before each iteration and delete unknown-type files after
  warning, so leftovers from a previous tag don't trigger
  repeated warnings or incorrect moves on subsequent tags.
- azure-pipelines-cd.yml: make the Mark-deployed step idempotent
  by checking for the marker tag via `git rev-parse --verify`
  before creating + pushing. Previously, a re-run after partial
  success would have aborted with `set -e` on the first
  pre-existing tag.
- .github/workflows/README.md: update the release-job description
  to reflect the atomic `gh release create --target <sha>` flow
  (the earlier docs still claimed the workflow pushed an annotated
  tag before creating the release).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- download-github-releases.mjs: fail fast with a clear message when
  `GH_TOKEN` is missing in non-`--check-only` mode (where `gh
  release download` would otherwise produce a generic auth error
  mid-loop).
- download-github-releases.mjs: clear `publish_artifacts_npm/`,
  `publish_artifacts_crates/`, and `publish_artifacts_meta/` at the
  start of non-check mode (in addition to the per-iteration
  `publish_artifacts_stage/` cleanup) so stale `.tgz`/`.crate`
  files from a partially-failed previous run cannot be reused.
- download-github-releases.mjs: refuse to mark a tag as processed
  when its release contained no `.tgz` or `.crate` assets. Without
  this the Azure pipeline would push the `deployed/<tag>` marker
  for a release that never actually published anything, so the
  problem would never be retried.
- download-github-releases.mjs + create-github-releases.mjs:
  narrow the `catch (error)` payload with
  `error instanceof Error ? error.message : String(error)` so we do
  not log `undefined` (or crash trying to read `.message`) if a
  non-`Error` value is thrown.
- create-github-releases.mjs: fail fast with a clear message when
  `GH_TOKEN` is missing in non-`--check-only` mode (the script
  needs it to call `gh release create`).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu force-pushed the users/janechu/update-cd-pipeline branch from aa73f47 to 3ad3ca6 Compare May 26, 2026 20:32
@janechu janechu requested a review from Copilot May 26, 2026 20:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.

Comment thread azure-pipelines-cd.yml Outdated
Comment thread azure-pipelines-cd.yml
- Replace the DST-fragile `1am PST = 09:00 UTC` cron comment with a
  UTC-anchored phrasing that also acknowledges the \~1am PT / \~2am
  PDT seasonal drift, so readers do not assume the schedule tracks
  Pacific time year-round.
- Drop the unused `undeployedTags` stage variable from the Package
  stage. It was bound to the Check stage output but never consumed
  downstream — the Package stage's own `download-github-releases.mjs`
  invocation re-enumerates undeployed releases from the local git tag
  set, so removing the captured variable also removes the implicit
  expectation that the two stages share that list. The script still
  emits the output via `##vso[task.setvariable]` for visibility in
  the Check stage log.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu
Copy link
Copy Markdown
Collaborator Author

janechu commented May 26, 2026

Addressed the two new review comments in 1261d1c:

  • azure-pipelines-cd.yml line 5 — Replaced the DST-fragile 1am PST = 09:00 UTC comment with a UTC-anchored phrasing that also acknowledges the seasonal Pacific drift: 09:00 UTC daily (~1am Pacific in standard time, ~2am during DST).. Readers no longer have to do the conversion in their head, and the schedule is documented in the same timezone Azure actually evaluates the cron in.
  • azure-pipelines-cd.yml line 83 — Removed the unused undeployedTags stage variable from the Package stage. It was being bound from the Check stage's output but never consumed downstream — the Package stage's own download-github-releases.mjs invocation re-enumerates undeployed releases from the local git tag set, so dropping the captured variable also removes the implicit expectation that the two stages share that list. download-github-releases.mjs still emits the Azure output variable in the Check stage so the detected tag list remains visible in the pipeline UI for human debugging.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.

Comment thread azure-pipelines-cd.yml
Previously the Azure pipeline created the marker with a bare
`git tag "${DEPLOY_TAG}"`, which anchors the marker to the agent's
current HEAD. That is usually correct on the first run (since the
release tag was created on the same main HEAD that the pipeline
checks out shortly after), but it is fragile on re-runs: if the
pipeline is replayed weeks later from a newer commit, the marker
would silently point at an unrelated commit, breaking the audit
trail that links a publication back to the exact source the
tarball was built from.

Use `git tag "${DEPLOY_TAG}" "refs/tags/${tag}"` so the marker
always anchors to the same commit as its release tag, regardless
of when the mark-deployed step runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu
Copy link
Copy Markdown
Collaborator Author

janechu commented May 26, 2026

Addressed the new review comment in 51302d4:

  • azure-pipelines-cd.yml line 142git tag "${DEPLOY_TAG}" was anchoring the deployed/ marker to the agent's current HEAD. On a first run that happens to be the same commit as the release tag, but a replay (e.g. weeks later from a newer commit) would silently move the marker to an unrelated SHA and break the audit trail. Replaced with git tag "${DEPLOY_TAG}" "refs/tags/${tag}" so the marker always anchors to the same commit as its release tag, regardless of when the mark-deployed step runs. The fetchTags: true on the Package job's checkout step makes the release tag's ref reliably available locally.

Verified the rerun-safety property in a local sandbox: with the release tag at commit A and HEAD at a later commit B, the new code anchors deployed/<tag> at A (not B). The refs/tags/${tag} rev-parse path also handles both lightweight and annotated tags transparently.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.

Comment thread CONTRIBUTING.md Outdated
The reviewer rightly pointed out the prior phrasing ("does not yet
exist on `origin`") implied the script queries the remote, while
`create-github-releases.mjs` actually just runs
`git rev-parse --verify refs/tags/<tag>` against the local refs.
Reword the preview command's description to say "is not present in
the local git tag list" and add a note suggesting
`git fetch --tags --prune origin` for contributors who want the
preview to reflect remote state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu
Copy link
Copy Markdown
Collaborator Author

janechu commented May 26, 2026

Addressed in a41e940e1:

  • CONTRIBUTING.md line 190 — Reworded the --check-only preview description to drop the misleading "does not yet exist on `origin`" phrasing. create-github-releases.mjs only consults git rev-parse refs/tags/<tag> against the local refs, so the docs now say "is not present in the local git tag list" and add a hint to run git fetch --tags --prune origin first if the contributor wants the preview to reflect remote state. The note also acknowledges that for a fresh bump that hasn't been pushed yet, the local tag list is the authoritative source anyway, so the fetch is only relevant when reviewing an in-flight bump PR locally.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.

Comment thread build/scripts/download-github-releases.mjs Outdated
Comment thread build/scripts/create-github-releases.mjs
…tion

- download-github-releases.mjs: tighten RELEASE_TAG_RE to
  `/_v\d+\.\d+\.\d+(?:-[\w.-]+)?$/` so it matches the complete
  beachball release tag format (including optional dot-separated
  prerelease) anchored at end-of-string. The prior unanchored regex
  would treat any tag containing `_v<semver>` somewhere in the
  middle as a release tag, which could have caused the Azure CD
  pipeline to attempt a `gh release download` for an unintended
  tag.

Also patched the PR body (no commit) to drop the stale
"pushes an annotated tag" and `gh release create --verify-tag`
claims, replacing them with the actual atomic
`gh release create --target <sha>` flow the script implements,
and rewrote the DST-fragile cron description to be UTC-anchored.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu
Copy link
Copy Markdown
Collaborator Author

janechu commented May 26, 2026

Addressed both new review comments in e95865bc4:

  • download-github-releases.mjs line 101 — Tightened RELEASE_TAG_RE from /_v\\d+\\.\\d+\\.\\d+/ (unanchored) to /_v\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?$/ (anchored at end, optional dot-separated prerelease), so the regex now matches the complete beachball tag format (${name}_v${major}.${minor}.${patch} with optional -prerelease) and refuses anything with trailing junk. Verified all 70+ existing release-format tags on microsoft/fast (including prereleases like @microsoft/fast-html_v1.0.0-alpha.53) still match, and that the deployed/\* filter remains a separate gate as before.
  • create-github-releases.mjs line 259 (PR description vs implementation) — Updated the PR description rather than the script. The script intentionally relies on gh release create --target <sha> to create the lightweight tag atomically with the release (which is what gives us the no-orphan-tag guarantee — if gh release create fails, neither the tag nor the release exists, so the next workflow run cleanly retries). Patched the PR body to drop the stale "pushes an annotated tag" / --verify-tag claim and replace it with a description of the atomic-create flow, and also took the chance to remove the DST-fragile "1am PST" wording from the cron section so the body now matches the new azure-pipelines-cd.yml comment.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated no new comments.

Drop the `push: branches: [main]` trigger from
`.github/workflows/cd-github-releases.yml`. The workflow now runs
only on:

- A nightly cron at `0 8 * * *` UTC (~12am Pacific in standard time,
  drifting an hour during US daylight time), and
- `workflow_dispatch` for manual runs.

This puts the release-creation cron ~1 hour ahead of the existing
Azure CD pipeline (`0 9 * * *` UTC), so any GitHub release this
workflow creates is ready in time for that same night's
`npm publish` / `cargo publish` cycle. Removing the push trigger
means the workflow no longer runs on every `main` commit (so
non-bump merges no longer kick off a no-op release detect job).

Updated the surrounding docs to match:

- `.github/workflows/README.md` — the GitHub Actions trigger line
  is now "nightly cron + workflow_dispatch" instead of
  "push to main + workflow_dispatch".
- `CONTRIBUTING.md` Publishing > After merge — explains that the
  release is created by the next nightly cron and offers
  `gh workflow run cd-github-releases.yml` as the manual alternative
  for maintainers who don't want to wait.

The Azure pipeline is intentionally unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu merged commit 9fdd2a5 into main May 26, 2026
14 checks passed
@janechu janechu deleted the users/janechu/update-cd-pipeline branch May 26, 2026 22:31
@janechu janechu mentioned this pull request May 27, 2026
5 tasks
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.

3 participants