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
6 changes: 4 additions & 2 deletions .github/workflows/csharp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ jobs:
--tag-prefix "csharp-v" \
--language "C#" \
--package-id "${{ steps.package.outputs.id }}" \
--changelog-path "csharp/CHANGELOG.md"
--changelog-path "csharp/CHANGELOG.md" \
--assets-glob "csharp/artifacts/*.nupkg"

# === MANUAL INSTANT RELEASE ===
instant-release:
Expand Down Expand Up @@ -397,4 +398,5 @@ jobs:
--tag-prefix "csharp-v" \
--language "C#" \
--package-id "${{ steps.package.outputs.id }}" \
--changelog-path "csharp/CHANGELOG.md"
--changelog-path "csharp/CHANGELOG.md" \
--assets-glob "csharp/artifacts/*.nupkg"
64 changes: 41 additions & 23 deletions .github/workflows/wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,10 @@ on:
- 'js/**'
- 'rust/**'
workflow_dispatch:
inputs:
deploy_pages:
description: 'Deploy the built app to GitHub Pages'
required: true
default: false
type: boolean

concurrency:
group: wasm-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

permissions:
contents: read
Expand All @@ -35,6 +29,7 @@ jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -87,21 +82,14 @@ jobs:
name: link-cli-web
path: dist/

deploy:
name: Deploy GitHub Pages
build-pages:
name: Build GitHub Pages app
if: |
github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main' &&
inputs.deploy_pages
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'workflow_dispatch'
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
timeout-minutes: 30

steps:
- uses: actions/checkout@v4
Expand All @@ -121,22 +109,52 @@ jobs:
cache: npm
cache-dependency-path: js/package-lock.json

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
rust/target
rust/wasm/target
key: ${{ runner.os }}-wasm-pages-cargo-${{ hashFiles('rust/wasm/Cargo.lock', 'rust/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-wasm-pages-cargo-
${{ runner.os }}-wasm-cargo-

- name: Install npm dependencies
working-directory: js
run: npm ci

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Build GitHub Pages app
working-directory: js
run: npm run build:pages

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: dist/

- name: Deploy to GitHub Pages
deploy-pages:
name: Deploy GitHub Pages
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'workflow_dispatch'
needs: build-pages
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy Pages artifact
id: deployment
uses: actions/deploy-pages@v4
76 changes: 59 additions & 17 deletions csharp/scripts/create-github-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
/**
* Create GitHub Release from CHANGELOG.md
* Usage:
* node csharp/scripts/create-github-release.mjs --release-version <version> --repository <owner/repo> [--tag-prefix v] [--changelog-path CHANGELOG.md]
* node csharp/scripts/create-github-release.mjs --release-version <version> --repository <owner/repo> [--tag-prefix v] [--changelog-path CHANGELOG.md] [--assets-glob csharp/artifacts/*.nupkg]
*/

import { readFileSync, existsSync } from 'fs';
import { readFileSync, existsSync, readdirSync } from 'fs';
import { execFileSync } from 'child_process';
import { dirname, basename, join, isAbsolute } from 'path';

// Simple argument parsing
const args = process.argv.slice(2);
Expand All @@ -23,8 +24,32 @@ const tagPrefix = getArg('tag-prefix', 'v');
const changelogPath = getArg('changelog-path', 'CHANGELOG.md');
const language = getArg('language', '');
const packageId = getArg('package-id', '');
const assetsGlob = getArg('assets-glob', '');
const dryRun = args.includes('--dry-run');

/**
* Resolve a simple `directory/*.ext` glob to a list of file paths.
* Only `*` in the file name part is supported; matches are returned in name order.
*/
function resolveAssets(pattern) {
if (!pattern) return [];
const dir = dirname(pattern) || '.';
const filePattern = basename(pattern);
if (!existsSync(dir)) return [];

if (!filePattern.includes('*')) {
const candidate = isAbsolute(pattern) ? pattern : join(dir, filePattern);
return existsSync(candidate) ? [candidate] : [];
}

const escaped = filePattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
const regex = new RegExp(`^${escaped}$`);
return readdirSync(dir)
.filter((name) => regex.test(name))
.sort()
.map((name) => join(dir, name));
}

if (!version || !repository) {
console.error('Error: Missing required arguments');
console.error(
Expand Down Expand Up @@ -81,30 +106,47 @@ try {
process.exit(0);
}

const assetPaths = resolveAssets(assetsGlob);

let releaseExists = false;
try {
execFileSync('gh', ['release', 'view', tag, '--repo', repository], {
stdio: 'ignore',
});
console.log(`Release ${tag} already exists, skipping`);
process.exit(0);
releaseExists = true;
console.log(`Release ${tag} already exists, will reconcile assets`);
} catch {
// Release does not exist yet.
}

try {
execFileSync('gh', ['api', `repos/${repository}/releases`, '-X', 'POST', '--input', '-'], {
input: JSON.stringify(payload),
encoding: 'utf-8',
stdio: ['pipe', 'inherit', 'inherit'],
});
console.log(`Created GitHub release: ${tag}`);
} catch (error) {
// Check if release already exists
if (error.message && error.message.includes('already exists')) {
console.log(`Release ${tag} already exists, skipping`);
} else {
throw error;
if (!releaseExists) {
try {
execFileSync('gh', ['api', `repos/${repository}/releases`, '-X', 'POST', '--input', '-'], {
input: JSON.stringify(payload),
encoding: 'utf-8',
stdio: ['pipe', 'inherit', 'inherit'],
});
console.log(`Created GitHub release: ${tag}`);
} catch (error) {
if (error.message && error.message.includes('already exists')) {
console.log(`Release ${tag} already exists, will reconcile assets`);
} else {
throw error;
}
}
}

if (assetPaths.length === 0) {
if (assetsGlob) {
console.log(`No assets matched ${assetsGlob}, skipping asset upload`);
}
} else {
console.log(`Uploading ${assetPaths.length} asset(s) to ${tag}`);
execFileSync(
'gh',
['release', 'upload', tag, ...assetPaths, '--clobber', '--repo', repository],
{ stdio: 'inherit' }
);
}
} catch (error) {
console.error('Error creating release:', error.message);
Expand Down
31 changes: 31 additions & 0 deletions csharp/scripts/release-scripts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,34 @@ test('create-github-release dry run uses tag prefix and component changelog', ()
assert.match(payload.body, /Fixed release automation\./);
assert.match(payload.body, /Package: `clink`/);
});

test('create-github-release dry run reports matching assets without uploading', () => {
const dir = mkdtempSync(join(tmpdir(), 'link-cli-release-assets-'));
const changelog = join(dir, 'CHANGELOG.md');
writeFileSync(
changelog,
'# Changelog\n\n## [2.4.0] - 2026-05-12\n\nFixed release automation.\n'
);
const artifacts = join(dir, 'artifacts');
mkdirSync(artifacts, { recursive: true });
writeFileSync(join(artifacts, 'clink.2.4.0.nupkg'), 'fake');
writeFileSync(join(artifacts, 'clink.2.4.0.snupkg'), 'fake');
writeFileSync(join(artifacts, 'unrelated.txt'), 'fake');

const stdout = runNode('csharp/scripts/create-github-release.mjs', [
'--release-version',
'2.4.0',
'--repository',
'link-foundation/link-cli',
'--tag-prefix',
'csharp-v',
'--changelog-path',
changelog,
'--assets-glob',
join(artifacts, '*.nupkg'),
'--dry-run',
]);
const payload = JSON.parse(stdout.slice(stdout.indexOf('{')));

assert.equal(payload.tag_name, 'csharp-v2.4.0');
});
83 changes: 83 additions & 0 deletions docs/case-studies/issue-79/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,86 @@ Local checks after the fix:
- `npm --prefix js run build`
- `cargo test --manifest-path rust/Cargo.toml --all-features`
- `dotnet test csharp/Foundation.Data.Doublets.Cli.sln`

## Follow-up (PR #81)

After PR 80 merged, @konard reported in
<https://github.com/link-foundation/link-cli/issues/79#issuecomment-4432584607>
that two requirements were still not met:

1. The post-merge `WebAssembly CI` run
<https://github.com/link-foundation/link-cli/actions/runs/25747032579>
had its `Deploy GitHub Pages` job skipped, so the live site was not
refreshed.
2. `csharp.yml` and `rust.yml` were not triggered at all by the merge.

### Root causes found in this round

- **Pages deploy was opt-in.** `wasm.yml`'s `deploy` job had
`if: github.event_name == 'workflow_dispatch' && ... && inputs.deploy_pages`,
so a normal push to `main` could never deploy. The `js` template's
`example-app.yml` already shows the right pattern: build the Pages artifact in
the build job (with `actions/configure-pages` + `upload-pages-artifact`) and
hand off to a dedicated `deploy-pages` job that only contains
`actions/deploy-pages@v4`. Evidence: `template-js-example-app.yml`.
- **`csharp.yml` / `rust.yml` did not match the changed paths.** The PR 80
merge commit (sha `9c93a27e`) only touched `.github/workflows/wasm.yml`,
`.gitignore`, root README files, `docs/**`, and the moved `js/**` tree —
nothing under `csharp/**` or `rust/**`. Both pipelines correctly skipped
themselves. Evidence: `gh api repos/link-foundation/link-cli/commits/9c93a27e`
shows zero matches for `^csharp/` or `^rust/` filenames.
This is by design and does not need a code change in the workflow triggers,
but the case study now records the analysis so the requirement is closed
with evidence rather than a guess.
- **GitHub Releases never carried the NuGet artifact.**
`csharp/scripts/create-github-release.mjs` only posted the release notes; the
`.nupkg` produced by `dotnet pack` was uploaded to nuget.org but never
attached to the GitHub Release. The same shortfall exists in the upstream
`csharp-ai-driven-development-pipeline-template`.

### Fixes applied in PR 81

- Split `wasm.yml` into `test`, `build-pages`, and `deploy-pages` jobs.
`build-pages` runs on every push to `main` (and on `workflow_dispatch`),
configures Pages, and uploads the artifact. `deploy-pages` only runs
`actions/deploy-pages@v4` with the required `pages: write` and
`id-token: write` permissions and the `github-pages` environment.
Removed the obsolete `deploy_pages` boolean input.
- Added a `--assets-glob` flag to `csharp/scripts/create-github-release.mjs`
that resolves a `dir/*.ext` pattern and calls
`gh release upload <tag> <files...> --clobber` after the release exists.
Wired `--assets-glob "csharp/artifacts/*.nupkg"` into both the auto release
and instant release jobs in `.github/workflows/csharp.yml`.
- Extended `js/test/repositoryLayout.test.mjs` with two regression tests:
one asserting `wasm.yml` deploys Pages on push to `main`, and one asserting
`csharp.yml` carries the asset glob in both release jobs.
- Added a unit test for the new asset glob in
`csharp/scripts/release-scripts.test.mjs` that uses `--dry-run` so it does
not contact GitHub.
- Removed the regenerated `.gitkeep` and the throwaway `ci-logs/` folder
used to download the wasm CI log; the log is preserved as
`evidence/wasm-25747032579.log`.

### Upstream report

Filed
<https://github.com/link-foundation/csharp-ai-driven-development-pipeline-template/issues/7>
covering the missing `.nupkg` upload in the C# template. The issue includes
the reproduction, the suggested fix, and a link back to PR 81 with the
working patch. Evidence:
`evidence/upstream-csharp-template-issue-7.json`.

### New evidence files

- `wasm-25747032579.log`, `run-25747032579.json`,
`run-25747032579-jobs.json` — the post-merge WebAssembly CI run that
triggered the followup.
- `issue-79-comments-followup.json` — captures the user's comment that
reopened the work.
- `upstream-csharp-template-issue-7.json` — confirmation that the upstream
issue was filed.

### Verification (PR 81)

- `node --test js/test/repositoryLayout.test.mjs`
- `node --test csharp/scripts/release-scripts.test.mjs`
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"url":"https://api.github.com/repos/link-foundation/link-cli/issues/comments/4432584607","html_url":"https://github.com/link-foundation/link-cli/issues/79#issuecomment-4432584607","issue_url":"https://api.github.com/repos/link-foundation/link-cli/issues/79","id":4432584607,"node_id":"IC_kwDONXCAbs8AAAABCDPfnw","user":{"login":"konard","id":1431904,"node_id":"MDQ6VXNlcjE0MzE5MDQ=","avatar_url":"https://avatars.githubusercontent.com/u/1431904?v=4","gravatar_id":"","url":"https://api.github.com/users/konard","html_url":"https://github.com/konard","followers_url":"https://api.github.com/users/konard/followers","following_url":"https://api.github.com/users/konard/following{/other_user}","gists_url":"https://api.github.com/users/konard/gists{/gist_id}","starred_url":"https://api.github.com/users/konard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/konard/subscriptions","organizations_url":"https://api.github.com/users/konard/orgs","repos_url":"https://api.github.com/users/konard/repos","events_url":"https://api.github.com/users/konard/events{/privacy}","received_events_url":"https://api.github.com/users/konard/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2026-05-12T16:21:17Z","updated_at":"2026-05-12T16:21:17Z","body":"https://github.com/link-foundation/link-cli/actions/runs/25747032579/job/75613378020 GitHub Pages deploy was skipped.\n\ncsharp.yml and rust.yml was not triggered at all. That is critical bug, that needs fixing, requirements are not fully met.\n\n","author_association":"MEMBER","pin":null,"reactions":{"url":"https://api.github.com/repos/link-foundation/link-cli/issues/comments/4432584607/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null}]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"total_count":2,"jobs":[{"id":75612958594,"run_id":25747032579,"workflow_name":"WebAssembly CI","head_branch":"main","run_url":"https://api.github.com/repos/link-foundation/link-cli/actions/runs/25747032579","run_attempt":1,"node_id":"CR_kwDONXCAbs8AAAARmuIvgg","head_sha":"9c93a27e662408102894dacb89c8ed841a151769","url":"https://api.github.com/repos/link-foundation/link-cli/actions/jobs/75612958594","html_url":"https://github.com/link-foundation/link-cli/actions/runs/25747032579/job/75612958594","status":"completed","conclusion":"success","created_at":"2026-05-12T16:12:43Z","started_at":"2026-05-12T16:12:52Z","completed_at":"2026-05-12T16:14:52Z","name":"Test","steps":[{"name":"Set up job","status":"completed","conclusion":"success","number":1,"started_at":"2026-05-12T16:12:53Z","completed_at":"2026-05-12T16:12:54Z"},{"name":"Run actions/checkout@v4","status":"completed","conclusion":"success","number":2,"started_at":"2026-05-12T16:12:54Z","completed_at":"2026-05-12T16:12:56Z"},{"name":"Setup Rust","status":"completed","conclusion":"success","number":3,"started_at":"2026-05-12T16:12:56Z","completed_at":"2026-05-12T16:13:07Z"},{"name":"Install wasm-pack","status":"completed","conclusion":"success","number":4,"started_at":"2026-05-12T16:13:07Z","completed_at":"2026-05-12T16:14:20Z"},{"name":"Setup Node.js","status":"completed","conclusion":"success","number":5,"started_at":"2026-05-12T16:14:20Z","completed_at":"2026-05-12T16:14:22Z"},{"name":"Cache cargo registry","status":"completed","conclusion":"success","number":6,"started_at":"2026-05-12T16:14:22Z","completed_at":"2026-05-12T16:14:29Z"},{"name":"Install npm dependencies","status":"completed","conclusion":"success","number":7,"started_at":"2026-05-12T16:14:29Z","completed_at":"2026-05-12T16:14:31Z"},{"name":"Test Rust CLI core","status":"completed","conclusion":"success","number":8,"started_at":"2026-05-12T16:14:31Z","completed_at":"2026-05-12T16:14:34Z"},{"name":"Test WebAssembly wrapper","status":"completed","conclusion":"success","number":9,"started_at":"2026-05-12T16:14:34Z","completed_at":"2026-05-12T16:14:39Z"},{"name":"Build React WebAssembly app","status":"completed","conclusion":"success","number":10,"started_at":"2026-05-12T16:14:39Z","completed_at":"2026-05-12T16:14:47Z"},{"name":"Upload built app","status":"completed","conclusion":"success","number":11,"started_at":"2026-05-12T16:14:47Z","completed_at":"2026-05-12T16:14:48Z"},{"name":"Post Cache cargo registry","status":"completed","conclusion":"success","number":20,"started_at":"2026-05-12T16:14:48Z","completed_at":"2026-05-12T16:14:48Z"},{"name":"Post Setup Node.js","status":"completed","conclusion":"success","number":21,"started_at":"2026-05-12T16:14:48Z","completed_at":"2026-05-12T16:14:49Z"},{"name":"Post Run actions/checkout@v4","status":"completed","conclusion":"success","number":22,"started_at":"2026-05-12T16:14:49Z","completed_at":"2026-05-12T16:14:49Z"},{"name":"Complete job","status":"completed","conclusion":"success","number":23,"started_at":"2026-05-12T16:14:49Z","completed_at":"2026-05-12T16:14:49Z"}],"check_run_url":"https://api.github.com/repos/link-foundation/link-cli/check-runs/75612958594","labels":["ubuntu-latest"],"runner_id":1000028948,"runner_name":"GitHub Actions 1000028948","runner_group_id":0,"runner_group_name":"GitHub Actions"},{"id":75613378020,"run_id":25747032579,"workflow_name":"WebAssembly CI","head_branch":"main","run_url":"https://api.github.com/repos/link-foundation/link-cli/actions/runs/25747032579","run_attempt":1,"node_id":"CR_kwDONXCAbs8AAAARmuiV5A","head_sha":"9c93a27e662408102894dacb89c8ed841a151769","url":"https://api.github.com/repos/link-foundation/link-cli/actions/jobs/75613378020","html_url":"https://github.com/link-foundation/link-cli/actions/runs/25747032579/job/75613378020","status":"completed","conclusion":"skipped","created_at":"2026-05-12T16:14:52Z","started_at":"2026-05-12T16:14:52Z","completed_at":"2026-05-12T16:14:52Z","name":"Deploy GitHub Pages","steps":[],"check_run_url":"https://api.github.com/repos/link-foundation/link-cli/check-runs/75613378020","labels":["ubuntu-latest"],"runner_id":null,"runner_name":null,"runner_group_id":null,"runner_group_name":null}]}
Loading
Loading