Skip to content
Open
38 changes: 19 additions & 19 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
name: AI Harness Scorecard

on:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1"
push:
branches: [main]
schedule:
- cron: "0 6 * * 1"

jobs:
scorecard:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: markmishaev76/ai-harness-scorecard@v1
id: scorecard
- name: Commit badge and report
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add scorecard-badge.json scorecard-report.md
git diff --cached --quiet || git commit -m "chore: update scorecard badge and report"
git push
scorecard:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: markmishaev76/ai-harness-scorecard@v1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and inspect the scorecard.yml file
find . -name "scorecard.yml" -o -name "*scorecard*" | grep -E "\.(yml|yaml)$" | head -20

Repository: anyulled/devbcn-nextjs

Length of output: 99


🏁 Script executed:

# Check if .github/workflows/scorecard.yml exists and read it
if [ -f .github/workflows/scorecard.yml ]; then
  cat -n .github/workflows/scorecard.yml
else
  echo "File not found at .github/workflows/scorecard.yml"
  # Try to find it in the repo
  find . -name "*scorecard*.yml" -o -name "*scorecard*.yaml"
fi

Repository: anyulled/devbcn-nextjs

Length of output: 894


🌐 Web query:

GitHub Actions security hardening pin third-party actions commit SHA

💡 Result:

To harden GitHub Actions against supply‑chain attacks, pin any third‑party action (and reusable workflow) to an immutable reference—preferably a full commit SHA rather than a mutable tag like @v1 or a branch like @main. GitHub explicitly notes that pinning to a full‑length commit SHA is currently the only way to use an action as an immutable release. [1]

Why pin to a commit SHA (vs tags)

  • Tags and branches can move (retagging/force-push), so uses: owner/action@v1 may execute different code later without changing your workflow file. GitHub recommends SHA pinning to reduce impact of compromised dependencies. [1][2]
  • SHA pinning helps prevent surprise changes and reduces risk from malicious tag updates. [3]

How to pin (example)

Instead of:

- uses: actions/checkout@v4

Use the exact commit:

- uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA>

(Replace with the commit that corresponds to the release you reviewed.) [1][3]

Enforce SHA pinning

  • GitHub provides repository/org/enterprise policies to require actions be pinned to a full-length commit SHA. [1]
  • You can also add a workflow check that fails PRs if actions aren’t SHA‑pinned (example: “Ensure SHA Pinned Actions”). [4]

Keeping pinned SHAs updated safely

  • Treat action updates like dependencies: periodically bump the pinned SHA to the next vetted release (often via Dependabot/automation + code review). This preserves immutability while still getting fixes. GitHub’s governance features and guidance emphasize proactive pinning and controlled updates. [2]

Sources

  1. GitHub Docs — Security hardening for GitHub Actions (pinning to full-length commit SHA; policies) https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/security-for-github-actions/security-guides/security-hardening-for-github-actions
  2. GitHub Changelog (2025‑08‑15) — Actions policy now supports blocking and SHA pinning actions https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions/
  3. GitHub Well‑Architected — Actions security recommendations (pin to immutable reference; commit SHA example) https://wellarchitected.github.com/library/application-security/recommendations/actions-security/
  4. GitHub Marketplace — Ensure SHA Pinned Actions https://github.com/marketplace/actions/ensure-sha-pinned-actions

Pin the third-party action to an immutable commit SHA.

This workflow grants contents: write permission to a third-party action and then commits and pushes back to the repository. GitHub's security guidance recommends pinning all third-party actions to a full-length commit SHA because tags like @v1 are mutable and can be retagged; a compromised action could exploit write access to introduce malicious code into your repository. Commit SHAs are the only immutable reference for actions.

🔒 Suggested hardening
-      - uses: markmishaev76/ai-harness-scorecard@v1
+      - uses: markmishaev76/ai-harness-scorecard@<full-length-commit-sha> # v1
         id: scorecard
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/scorecard.yml around lines 12 - 16, The workflow pins
third-party actions using mutable tags (actions/checkout@v6 and
markmishaev76/ai-harness-scorecard@v1); replace each tag with the corresponding
full commit SHA by looking up the exact commit in the action repos and updating
the uses entries to the full 40-character commit SHA for
markmishaev76/ai-harness-scorecard and for actions/checkout (or at minimum for
the third-party action) so the workflow references an immutable commit; ensure
you commit the updated workflow to the repo.

id: scorecard
- name: Commit badge and report
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add scorecard-badge.json scorecard-report.md
git diff --cached --quiet || git commit -m "chore: update scorecard badge and report"
git push
86 changes: 47 additions & 39 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,56 +28,64 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
});
}

for (const year of years) {
urls.push({
url: `${baseUrl}/${year}`,
lastModified: new Date(),
changeFrequency: "daily",
priority: 0.9,
});
// ⚡ Bolt: Fetch all years' data in parallel to significantly reduce sitemap generation time
await Promise.all(
years.map(async (year) => {
const yearUrls: MetadataRoute.Sitemap = [];

const yearPages = ["speakers", "talks", "schedule", "job-offers", "cfp", "diversity", "sponsorship", "travel"];
for (const page of yearPages) {
urls.push({
url: `${baseUrl}/${year}/${page}`,
yearUrls.push({
url: `${baseUrl}/${year}`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
changeFrequency: "daily",
priority: 0.9,
});
}

const speakers = await getSpeakers(year);
for (const speaker of speakers) {
urls.push({
url: `${baseUrl}/${year}/speakers/${speaker.id}`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.7,
});
}
const yearPages = ["speakers", "talks", "schedule", "job-offers", "cfp", "diversity", "sponsorship", "travel"];
for (const page of yearPages) {
yearUrls.push({
url: `${baseUrl}/${year}/${page}`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
});
}

// Fetch speakers and talks in parallel for each year, gracefully handling failures
const [speakers, sessionGroups] = await Promise.all([getSpeakers(year).catch(() => []), getTalks(year).catch(() => [])]);

const sessionGroups = await getTalks(year);
for (const group of sessionGroups) {
for (const talk of group.sessions) {
urls.push({
url: `${baseUrl}/${year}/talks/${talk.id}`,
for (const speaker of speakers) {
yearUrls.push({
url: `${baseUrl}/${year}/speakers/${speaker.id}`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.7,
});
}
}

const companies = getJobOffersByYear(year);
for (const company of companies) {
urls.push({
url: `${baseUrl}/${year}/job-offers/${slugify(company.name)}`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.5,
});
}
}
for (const group of sessionGroups) {
for (const talk of group.sessions) {
yearUrls.push({
url: `${baseUrl}/${year}/talks/${talk.id}`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.7,
});
}
}

const companies = getJobOffersByYear(year);
for (const company of companies) {
yearUrls.push({
url: `${baseUrl}/${year}/job-offers/${slugify(company.name)}`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.5,
});
}

urls.push(...yearUrls);
})
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

While the current implementation with urls.push(...yearUrls) inside the map callback will likely work correctly in Node.js's single-threaded event loop model, it relies on mutating a shared array from within concurrent asynchronous operations. This is generally considered an unsafe pattern that can be fragile and harder to reason about.

A more robust and functional approach is to have each asynchronous operation return its result, and then aggregate the results once all promises have been resolved. This avoids shared mutable state and makes the data flow more explicit.

  const yearlyUrls = await Promise.all(
    years.map(async (year) => {
      const yearUrls: MetadataRoute.Sitemap = [];

      yearUrls.push({
        url: `${baseUrl}/${year}`,
        lastModified: new Date(),
        changeFrequency: "daily",
        priority: 0.9,
      });

      const yearPages = ["speakers", "talks", "schedule", "job-offers", "cfp", "diversity", "sponsorship", "travel"];
      for (const page of yearPages) {
        yearUrls.push({
          url: `${baseUrl}/${year}/${page}`,
          lastModified: new Date(),
          changeFrequency: "weekly",
          priority: 0.8,
        });
      }

      // Fetch speakers and talks in parallel for each year, gracefully handling failures
      const [speakers, sessionGroups] = await Promise.all([getSpeakers(year).catch(() => []), getTalks(year).catch(() => [])]);

      for (const speaker of speakers) {
        yearUrls.push({
          url: `${baseUrl}/${year}/speakers/${speaker.id}`,
          lastModified: new Date(),
          changeFrequency: "weekly",
          priority: 0.7,
        });
      }

      for (const group of sessionGroups) {
        for (const talk of group.sessions) {
          yearUrls.push({
            url: `${baseUrl}/${year}/talks/${talk.id}`,
            lastModified: new Date(),
            changeFrequency: "weekly",
            priority: 0.7,
          });
        }
      }

      const companies = getJobOffersByYear(year);
      for (const company of companies) {
        yearUrls.push({
          url: `${baseUrl}/${year}/job-offers/${slugify(company.name)}`,
          lastModified: new Date(),
          changeFrequency: "monthly",
          priority: 0.5,
        });
      }

      return yearUrls;
    })
  );
  urls.push(...yearlyUrls.flat());


return urls;
}
2 changes: 1 addition & 1 deletion cypress/e2e/home/home-editions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe("Home Pages (2023-2026)", () => {
cy.visit(edition.path, { timeout: 120000 });

cy.get(".hero8-header", { timeout: 30000 }).within(() => {
cy.get("h5").should("have.length.at.least", 2);
cy.get(".hero8-header__event-line").should("have.length.at.least", 2);
cy.contains(edition.venue, { matchCase: false }).should("be.visible");
cy.contains(edition.date, { matchCase: false }).should("be.visible");
});
Expand Down
Loading
Loading