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
22 changes: 17 additions & 5 deletions .github/workflows/actions/release-info/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const fetch = require("node-fetch");
const hasha = require("hasha");
const fs = require("fs");
const path = require("path");
const { mergeRedirects } = require("./merge-redirects");

async function run() {
// Repo information
Expand Down Expand Up @@ -171,14 +172,25 @@ async function run() {

// Write the redirects
if (redirectPath && redirectTemplate) {
const redirOutput = [];
const downloadLines = [];
// Stable / latest release
redirOutput.push(...generateRedirects(redirects, redirectTemplate));
downloadLines.push(...generateRedirects(redirects, redirectTemplate));

// Unstable / latest prerelease
redirOutput.push(...generateRedirects(prereleaseProcessed.redirects, preRedirectTemplate));

const redirOut = redirOutput.join("\n");
downloadLines.push(...generateRedirects(prereleaseProcessed.redirects, preRedirectTemplate));

// The static path each template owns (text before the first $$ token),
// e.g. "/download/latest/" and "/download/prerelease/". Only lines under
// these namespaces are regenerated; all other lines are preserved.
const managedPrefixes = [redirectTemplate, preRedirectTemplate]
.filter(Boolean)
.map((t) => t.split("$$")[0]);

const existing = fs.existsSync(redirectPath)
? fs.readFileSync(redirectPath, "utf8")
: "";

const redirOut = mergeRedirects(existing, downloadLines, managedPrefixes);
console.log(`Writing redirects file to ${redirectPath}`);
console.log(`${redirOut}\n\n`);
fs.writeFileSync(redirectPath, redirOut);
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/actions/release-info/merge-redirects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Merge regenerated download redirect lines with the manually-maintained lines
// already present in a _redirects file. The action regenerates only the
// download namespaces (e.g. /download/latest/, /download/prerelease/); every
// other line (manual redirects, comments, blanks) is preserved.
function mergeRedirects(existingContent, downloadLines, managedPrefixes) {
const downloadBlock = downloadLines.join("\n");

const existingLines = existingContent ? existingContent.split("\n") : [];
const preserved = existingLines.filter((line) => {
const token = line.trimStart().split(/\s+/)[0];
return !managedPrefixes.some((prefix) => token.startsWith(prefix));
});

// Drop blank lines at the edges so re-runs stay stable.
while (preserved.length && preserved[0].trim() === "") preserved.shift();
while (preserved.length && preserved[preserved.length - 1].trim() === "") {
preserved.pop();
}

if (preserved.length === 0) {
return downloadBlock;
}
return downloadBlock + "\n\n" + preserved.join("\n");
}

module.exports = { mergeRedirects };
2 changes: 1 addition & 1 deletion .github/workflows/actions/release-info/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node test-merge-redirects.js"
},
"keywords": [],
"author": "",
Expand Down
70 changes: 70 additions & 0 deletions .github/workflows/actions/release-info/test-merge-redirects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const assert = require("assert");
const { mergeRedirects } = require("./merge-redirects");

const downloadLines = [
"/download/latest/quarto-win.msi https://example/v1/quarto-win.msi",
"/download/prerelease/quarto-win.msi https://example/v2/quarto-win.msi",
];
const managedPrefixes = ["/download/latest/", "/download/prerelease/"];

// Fixture: stale download lines + a manual blog block (comment + rules).
const existing = [
"/download/latest/quarto-win.msi https://example/OLD/quarto-win.msi",
"/download/prerelease/quarto-win.msi https://example/OLD2/quarto-win.msi",
"",
"# Quarto blog migrated",
"/docs/blog/posts/2022-02-13-feature-callouts/* https://opensource.posit.co/blog/2022-02-13_feature-callouts/ 301!",
"/docs/blog/ https://opensource.posit.co/blog/q/quarto/ 301!",
].join("\n");

const out = mergeRedirects(existing, downloadLines, managedPrefixes);

// 1. Manual content preserved.
assert(out.includes("# Quarto blog migrated"), "comment preserved");
assert(
out.includes("/docs/blog/posts/2022-02-13-feature-callouts/*"),
"per-post blog rule preserved"
);
assert(
out.includes("/docs/blog/ https://opensource.posit.co/blog/q/quarto/ 301!"),
"blog index rule preserved"
);

// 2. Download lines regenerated; stale ones gone.
assert(out.includes("https://example/v1/quarto-win.msi"), "new latest url present");
assert(out.includes("https://example/v2/quarto-win.msi"), "new prerelease url present");
assert(!out.includes("OLD"), "stale download urls removed");

// 3. Download block on top, manual block below.
assert(
out.indexOf("/download/latest/") < out.indexOf("# Quarto blog"),
"download block precedes manual block"
);

// 4. Idempotent: merging the output again yields identical content.
const out2 = mergeRedirects(out, downloadLines, managedPrefixes);
assert.strictEqual(out2, out, "mergeRedirects is idempotent");

// 5. Empty existing content -> download block only.
assert.strictEqual(
mergeRedirects("", downloadLines, managedPrefixes),
downloadLines.join("\n"),
"empty existing -> download block only"
);

// 6. No trailing newline (matches committed _redirects format).
assert(!out.endsWith("\n"), "no trailing newline");

// 7. Prefix derivation used by index.js: template text before the first $$.
assert.strictEqual(
"/download/latest/$$prefix$$-$$suffix$$.$$extension$$".split("$$")[0],
"/download/latest/",
"latest prefix derived from template"
);
assert.strictEqual(
"/download/prerelease/$$prefix$$-$$suffix$$.$$extension$$".split("$$")[0],
"/download/prerelease/",
"prerelease prefix derived from template"
);

console.log("All mergeRedirects tests passed");
20 changes: 14 additions & 6 deletions .github/workflows/update-downloads.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
outputs:
changes_detected: ${{ steps.auto-commit.outputs.changes_detected }}
commit: ${{ steps.auto-commit.outputs.commit_hash }}
commit_prerelease: ${{ steps.cherry-pick-prerelease.outputs.commit_hash }}
commit_prerelease: ${{ steps.sync-prerelease.outputs.commit_hash }}
prerelease_version: ${{ steps.get-versions.outputs.prerelease_version }}
steps:
- name: Checkout Repo
Expand Down Expand Up @@ -45,16 +45,24 @@ jobs:
commit_user_email: runner@quarto.org
commit_author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

- name: Cherry-pick change to prerelease branch
id: cherry-pick-prerelease
- name: Sync generated downloads to prerelease branch
id: sync-prerelease
if: ${{ steps.auto-commit.outputs.changes_detected == 'true' }}
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
MAIN_SHA=${{ steps.auto-commit.outputs.commit_hash }}
git checkout prerelease
git cherry-pick ${{ steps.auto-commit.outputs.commit_hash }}
git push origin prerelease
# set an output for github action with the resulted commit of cherry-pick command
# _redirects is authoritative on main (manual redirects reach prerelease
# via the backport workflow), so overwrite the generated files outright
# instead of cherry-picking — never conflicts.
git checkout "$MAIN_SHA" -- _redirects docs/download/_download.json docs/download/_prerelease.json
if git diff --cached --quiet; then
echo "No changes to sync to prerelease"
else
git commit -m "Sync generated download redirects from main"
git push origin prerelease
fi
echo "commit_hash=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

# If a new commit has been made with updated downloads,
Expand Down
49 changes: 48 additions & 1 deletion _redirects
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,51 @@
/download/prerelease/quarto-macos.tar.gz https://github.com/quarto-dev/quarto-cli/releases/download/v1.10.11/quarto-1.10.11-macos.tar.gz
/download/prerelease/quarto-win.msi https://github.com/quarto-dev/quarto-cli/releases/download/v1.10.11/quarto-1.10.11-win.msi
/download/prerelease/quarto-win.zip https://github.com/quarto-dev/quarto-cli/releases/download/v1.10.11/quarto-1.10.11-win.zip
/download/prerelease/quarto-. https://github.com/quarto-dev/quarto-cli/releases/download/v1.10.11/quarto-1.10.11.tar.gz
/download/prerelease/quarto-. https://github.com/quarto-dev/quarto-cli/releases/download/v1.10.11/quarto-1.10.11.tar.gz

# Quarto blog migrated to the Posit Open Source site (2026-05).
# Per-post redirects first, then index and RSS feed.
/docs/blog/posts/2022-02-13-feature-callouts/* https://opensource.posit.co/blog/2022-02-13_feature-callouts/ 301!
/docs/blog/posts/2022-02-15-feature-tables/* https://opensource.posit.co/blog/2022-02-15_feature-tables/ 301!
/docs/blog/posts/2022-02-17-advanced-layout/* https://opensource.posit.co/blog/2022-02-17_advanced-layout/ 301!
/docs/blog/posts/2022-06-21-rstudio-conf-2022-quarto/* https://opensource.posit.co/blog/2022-06-21_rstudio-conf-2022-quarto/ 301!
/docs/blog/posts/2022-07-25-feature-extensions/* https://opensource.posit.co/blog/2022-07-25_feature-extensions/ 301!
/docs/blog/posts/2022-10-25-shinylive-extension/* https://opensource.posit.co/blog/2022-10-25_shinylive-extension/ 301!
/docs/blog/posts/2023-03-13-code-annotation/* https://opensource.posit.co/blog/2023-03-13_code-annotation/ 301!
/docs/blog/posts/2023-03-15-multi-format/* https://opensource.posit.co/blog/2023-03-15_multi-format/ 301!
/docs/blog/posts/2023-03-17-jupyter-cell-embedding/* https://opensource.posit.co/blog/2023-03-17_jupyter-cell-embedding/ 301!
/docs/blog/posts/2023-03-20-confluence/* https://opensource.posit.co/blog/2023-03-20_confluence/ 301!
/docs/blog/posts/2023-04-26-1.3-release/* https://opensource.posit.co/blog/2023-04-26_1.3-release/ 301!
/docs/blog/posts/2023-05-15-get-started/* https://opensource.posit.co/blog/2023-05-15_get-started/ 301!
/docs/blog/posts/2023-05-22-quarto-for-academics/* https://opensource.posit.co/blog/2023-05-22_quarto-for-academics/ 301!
/docs/blog/posts/2023-12-05-asa-traveling-courses/* https://opensource.posit.co/blog/2023-12-05_asa-traveling-courses/ 301!
/docs/blog/posts/2023-12-07-quarto-dashboards-demo/* https://opensource.posit.co/blog/2023-12-07_quarto-dashboards-demo/ 301!
/docs/blog/posts/2024-01-24-1.4-release/* https://opensource.posit.co/blog/2024-01-24_1.4-release/ 301!
/docs/blog/posts/2024-03-26-hugging-face/* https://opensource.posit.co/blog/2024-03-26_hugging-face/ 301!
/docs/blog/posts/2024-04-01-manuscripts-rmedicine/* https://opensource.posit.co/blog/2024-04-01_manuscripts-rmedicine/ 301!
/docs/blog/posts/2024-05-28-conf-workshops/* https://opensource.posit.co/blog/2024-05-28_conf-workshops/ 301!
/docs/blog/posts/2024-07-02-beautiful-tables-in-typst/* https://opensource.posit.co/blog/2024-07-02_beautiful-tables-in-typst/ 301!
/docs/blog/posts/2024-07-11-1.5-release/* https://opensource.posit.co/blog/2024-07-11_1.5-release/ 301!
/docs/blog/posts/2024-10-15-conf-workshops-materials/* https://opensource.posit.co/blog/2024-10-15_conf-workshops-materials/ 301!
/docs/blog/posts/2024-11-06-conf-talks/* https://opensource.posit.co/blog/2024-11-06_conf-talks/ 301!
/docs/blog/posts/2024-11-22-dashboards-workshop/* https://opensource.posit.co/blog/2024-11-22_dashboards-workshop/ 301!
/docs/blog/posts/2024-11-25-1.6-release/* https://opensource.posit.co/blog/2024-11-25_1.6-release/ 301!
/docs/blog/posts/2024-12-04-websites-workshop/* https://opensource.posit.co/blog/2024-12-04_websites-workshop/ 301!
/docs/blog/posts/2024-12-12-includes-meta/* https://opensource.posit.co/blog/2024-12-12_includes-meta/ 301!
/docs/blog/posts/2025-01-15-quarto-tip-brand-positron/* https://opensource.posit.co/blog/2025-01-15_quarto-tip-brand-positron/ 301!
/docs/blog/posts/2025-04-28-1.7-release/* https://opensource.posit.co/blog/2025-04-28_1.7-release/ 301!
/docs/blog/posts/2025-05-19-quarto-codespaces/* https://opensource.posit.co/blog/2025-05-19_quarto-codespaces/ 301!
/docs/blog/posts/2025-07-24-parameterized-reports-python/* https://opensource.posit.co/blog/2025-07-24_parameterized-reports-python/ 301!
/docs/blog/posts/2025-07-28-R-package-release-1.5/* https://opensource.posit.co/blog/2025-07-28_R-package-release-1.5/ 301!
/docs/blog/posts/2025-10-13-1.8-release/* https://opensource.posit.co/blog/2025-10-13_1.8-release/ 301!
/docs/blog/posts/2025-10-20-quarto-wizard-1-0-0/* https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/ 301!
/docs/blog/posts/2025-10-27-conf-workshops-materials/* https://opensource.posit.co/blog/2025-10-27_conf-workshops-materials/ 301!
/docs/blog/posts/2025-11-24-conf-talk-videos/* https://opensource.posit.co/blog/2025-11-24_conf-talk-videos/ 301!
/docs/blog/posts/2026-03-05-pdf-accessibility-and-standards/* https://opensource.posit.co/blog/2026-03-05_pdf-accessibility-and-standards/ 301!
/docs/blog/posts/2026-03-24-1.9-release/* https://opensource.posit.co/blog/2026-03-24_1.9-release/ 301!
/docs/blog/posts/2026-03-31-typst-books-and-more/* https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/ 301!
/docs/blog/posts/2026-04-06-whats-next-quarto-2/* https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2/ 301!
/docs/blog/posts/2026-04-14-chrome-headless-shell/* https://opensource.posit.co/blog/2026-04-14_chrome-headless-shell/ 301!
/docs/blog/posts/2026-05-07-quarto-2-parsing/* https://opensource.posit.co/blog/2026-05-07_quarto-2-parsing/ 301!
/docs/blog/index.xml https://opensource.posit.co/blog/index.xml 301!
/docs/blog/ https://opensource.posit.co/blog/q/quarto/ 301!
Loading