diff --git a/.github/DISCUSSION_TEMPLATE/accessibility.yml b/.github/DISCUSSION_TEMPLATE/accessibility.yml index 453177762..6554abbc5 100644 --- a/.github/DISCUSSION_TEMPLATE/accessibility.yml +++ b/.github/DISCUSSION_TEMPLATE/accessibility.yml @@ -2,13 +2,12 @@ labels: [Accessibility] body: - type: markdown attributes: - value: - - ⚠️ This category is solely for discussions and feedback on [the digital accessibility of GitHub products](https://accessibility.github.com/). ⚠️ + value: + ⚠️ This category is solely for discussions and feedback on [the digital accessibility of GitHub products](https://accessibility.github.com/). ⚠️ Please use this space for questions, bug reports, opportunities, and feedback related to making GitHub features accessible to users of all abilities. - - To ensure you receive a timely response and connect with the proper team(s), please post in the correct category, questions about access, 2FA, and SMS belong in the [General category](https://github.com/orgs/community/discussions/categories/general?discussions_q=is%3Aopen+category%3AGeneral). Discussions that are posted in the wrong category may be moved. Users who post spam or violate of our [Code of Conduct](https://github.com/github/.github/blob/95e9dcb0b8b70a25cdb75de29f600812a07eb996/CODE_OF_CONDUCT.md) will be blocked. + + To ensure you receive a timely response and connect with the proper team(s), please post in the correct category, questions about access, 2FA, and SMS belong in the [General category](https://github.com/orgs/community/discussions/categories/general?discussions_q=is%3Aopen+category%3AGeneral). Discussions that are posted in the wrong category may be moved. Users who post spam or violate of our [Code of Conduct](https://github.com/github/.github/blob/95e9dcb0b8b70a25cdb75de29f600812a07eb996/CODE_OF_CONDUCT.md) will be blocked. - type: dropdown attributes: label: Select Topic Area diff --git a/.github/DISCUSSION_TEMPLATE/actions.yml b/.github/DISCUSSION_TEMPLATE/actions.yml index f1c3bf3a2..20add0190 100644 --- a/.github/DISCUSSION_TEMPLATE/actions.yml +++ b/.github/DISCUSSION_TEMPLATE/actions.yml @@ -1,20 +1,64 @@ labels: [Actions] body: +- type: markdown + attributes: + value: | + 🚀 **Welcome to the Actions Category!** 🚀 + + Automate, customize, and execute your software development workflows right in your repository with [GitHub Actions](https://docs.github.com/en/actions)! + + **Why are you here?** + - **Have a Question?** Maybe you're stuck on workflow syntax, curious about best practices, or want to know how to use a specific GitHub Actions feature. Select "Question" to get help from the community! + - **Product Feedback?** Do you have ideas to improve GitHub Actions, or want to share what works (or doesn't) for you? Select "Product Feedback" to help shape the future of GitHub Actions. + - **Found a Bug?** If something isn't working as expected, let us know by selecting "Bug" so we can investigate and improve the platform. + + After choosing your reason, pick the topic or product area that best matches your discussion. + + **Quick links:** [Actions Documentation](https://docs.github.com/en/actions), [Workflow Syntax Guide](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions), [Example Workflows](https://docs.github.com/en/actions/use-cases-and-examples), and [Security Best Practices](https://docs.github.com/en/actions/security-for-github-actions) + + **Recent News:** + - [Latest Breaking Changes](https://github.blog/changelog/label/actions) + - type: dropdown + id: reason attributes: - label: Select Topic Area - description: What would you like to discuss? + label: Why are you starting this discussion? + description: Tell us your goal! Are you asking a question, giving feedback, reporting a bug, or sharing something cool? options: - "Question" - "Product Feedback" - "Bug" - - "Show & Tell" - - "General" + default: 0 validations: required: true +- type: dropdown + id: topic + attributes: + label: What GitHub Actions topic or product is this about? + description: | + Choose the single topic or product area most relevant to your discussion. Only one can be selected here. + + If your post covers additional themes or topics, don't worry! Our team and automation will do our best to add extra labels based on the content of your discussion details. + + This helps us get you the best answers and feedback! + options: + - "ARC (Actions Runner Controller)" + - "Actions Runner Image" + - "Actions Runner" + - "Actions Checkout" + - "Actions Cache" + - "Workflow Deployment" + - "Workflow Configuration" + - "Schedule & Cron Jobs" + - "Metrics & Insights" + - "Misc" + default: 9 + validations: + required: false - type: textarea attributes: - label: Body - description: Start your discussion! + label: Discussion Details + description: >- + Share your question, feedback, or story! Include links, code, or screenshots for more context. Tip: Reference the docs https://docs.github.com/en/actions or relevant changelogs for details. validations: - required: true + required: true diff --git a/.github/DISCUSSION_TEMPLATE/copilot-conversations.yml b/.github/DISCUSSION_TEMPLATE/copilot-conversations.yml new file mode 100644 index 000000000..5f25b77cb --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/copilot-conversations.yml @@ -0,0 +1,49 @@ +body: +- type: markdown + attributes: + value: | + #### Explore our [Copilot FAQs](https://github.com/orgs/community/discussions/144674) and [GitHub Docs](https://docs.github.com/en/copilot) for comprehensive guidance! + - 💡 **Quick Tip:** For general development questions or detailed code issues, try [Chat with Copilot](https://docs.github.com/en/enterprise-cloud@latest/copilot/using-github-copilot/copilot-chat/asking-github-copilot-questions-in-github) first, it might have the answer you need before posting here. + - 🛠️ For bugs and feature requests related to Copilot in VS Code, please open an issue in our [VS Code repo](https://github.com/microsoft/vscode/issues). If your issue is related to Copilot Chat, please [visit our FAQ](https://aka.ms/vscode-report-ai-feedback). + - 🔍 Are you a student/teacher looking to enable Copilot Pro? [Read our guide](https://gh.io/faq-nd-copilot) +- type: dropdown + attributes: + label: Select Topic Area + description: What would you like to discuss about GitHub Copilot? Choose your topic area. + options: + - Question + - Product Feedback + - Bug + - General + validations: + required: true +- type: dropdown + id: feature-area + attributes: + label: Copilot Feature Area + description: Which Copilot feature or product area is your discussion related to? + options: + - General + - Copilot in GitHub + - Copilot Workspace + - Copilot Agent Mode + - Copilot Coding Agent + - Copilot Enterprise + - Account Related + - VS Code + - Visual Studio + - JetBrains & Xcode + validations: + required: true +- type: textarea + attributes: + label: Body + description: Start your discussion! + placeholder: >- + Provide a detailed description of your discussion (e.g., What issue are you facing? What feedback do you have? What question do you need answered?) + validations: + required: true +- type: markdown + attributes: + value: | + #### Don't forget to mark an [answer as "solved"](https://docs.github.com/en/discussions/collaborating-with-your-community-using-discussions/participating-in-a-discussion#marking-a-comment-as-an-answer) to help others with similar questions. ✅ diff --git a/.github/DISCUSSION_TEMPLATE/discussions.yml b/.github/DISCUSSION_TEMPLATE/discussions.yml index c6547585f..22ad7c981 100644 --- a/.github/DISCUSSION_TEMPLATE/discussions.yml +++ b/.github/DISCUSSION_TEMPLATE/discussions.yml @@ -1,28 +1,43 @@ +title: "" labels: [Discussions] body: -- type: markdown - attributes: - value: | - ⚠️ This category is solely for discussions and feedback on [the GitHub Discussions product](https://github.com/features/discussions). ⚠️ + - type: markdown + attributes: + value: | + ⚠️ This category is solely for discussions and feedback on [the GitHub Discussions product](https://github.com/features/discussions). ⚠️ - The Discussions category is NOT meant to be a place for any old discussion regarding GitHub or programming — [the General category](https://github.com/orgs/community/discussions/categories/general) is a more appropriate space for misc topics. To ensure you receive a timely response and connect with the proper team(s), please post in the correct category by using the corresponding product category. + [GitHub Discussions](https://github.com/features/discussions) is a product within GitHub that allows users to talk, ask questions, and share ideas. Discussions are meant for open-ended conversations. - NOTE: If you post in this category about topics other than GitHub Discussions, your post may be moved or deleted without warning. Users who post spam or violate of our [Code of Conduct](https://github.com/github/.github/blob/95e9dcb0b8b70a25cdb75de29f600812a07eb996/CODE_OF_CONDUCT.md) will be blocked. -- type: dropdown - attributes: - label: Select Topic Area - description: What would you like to discuss? - options: - - "Question" - - "Product Feedback" - - "Bug" - - "Show & Tell" - - "General" - validations: - required: true -- type: textarea - attributes: - label: Body - description: Start your discussion! - validations: - required: true + ### ✅ Dos - Use this category if your post is about + - How to enable or configure **GitHub Discussions** in a repository or organization + - Questions about **GitHub Discussions** features, such as marking answers, organizing threads or using moderation tools. + - Bugs or issues specifically related to **GitHub Discussions** 🐞 + - Suggestions or feedback to improve the **GitHub Discussions** product 💬 + + ### 🚫 Don'ts - Please, don't use this category for: + - General troubleshooting, water cooler conversations or off-topic questions - have you tried General or Programming Help? + - Feedback that doesn't directly relate to _GitHub Discussions_, that feedback belong somewhere else, use this [quick guide](https://github.com/orgs/community/discussions/151702) to help. + + ### 📚 Need help understanding how GitHub Discussions works? + Read the official documentation: [GitHub Discussions Docs](https://docs.github.com/en/discussions) + + - type: dropdown + attributes: + label: "Discussion Type" + description: What would you like to discuss? + options: + - "Product Feedback" + - "Question" + - "Bug" + - "Show & Tell" + - "General" + validations: + required: true + - type: textarea + attributes: + label: "Discussion Content" + description: Start your discussion! + validations: + pattern: "^.{1000,}$" + regexFlags: "s" + note: "Must have at least 1000 characters" diff --git a/.github/DISCUSSION_TEMPLATE/general.yml b/.github/DISCUSSION_TEMPLATE/general.yml index 71f42bc02..3a64c3d6d 100644 --- a/.github/DISCUSSION_TEMPLATE/general.yml +++ b/.github/DISCUSSION_TEMPLATE/general.yml @@ -1,5 +1,13 @@ -labels: [general] +labels: [General] body: +- type: markdown + attributes: + value: + ⚠️ The Community can not help with account-related issues, such as reinstatement requests or help to escalate your [GitHub Support](https://support.github.com/contact) tickets. ⚠️ +- type: markdown + attributes: + value: + 📛 We made the decision to disable the ability to earn Achievements in our Community in order to discourage users from participating in coordinated or inauthentic activity like rapid questions and answers in order to earn badges. You can learn more about this decision in our announcement post here [Achievements will no longer be available in the Community](https://github.com/orgs/community/discussions/106536) 📛 - type: dropdown attributes: label: Select Topic Area diff --git a/.github/DISCUSSION_TEMPLATE/github-education.yml b/.github/DISCUSSION_TEMPLATE/github-education.yml index c2d50bccf..e34d1bfb7 100644 --- a/.github/DISCUSSION_TEMPLATE/github-education.yml +++ b/.github/DISCUSSION_TEMPLATE/github-education.yml @@ -2,34 +2,36 @@ body: - type: markdown attributes: value: | - ## We’re really excited you’re here! 👋 🎉 Welcome to the GitHub Education community! 🎒 + #### Welcome! Before You Create a New Post... - type: markdown attributes: value: | - Before jumping into your conversation please read the hints and tips below to help you create a great discussion topic others would more likely respond to 💬 + Please read the **hints and tips** below. Your question might already have an answer waiting for you! + If you are new here and are unsure where to start, please check out our [Quick Start Guide](https://github.com/orgs/community/discussions/141947). 👈 - type: markdown attributes: value: | - #### Are you asking for help with your GitHub Global Campus application process? - * **Help us to help you** 👍 Provide as much relevant information about your application as possible including posting your **latest rejection reasons**. - * Do **not post** any personal information on the discussions boards e.g images of your evidence. + ### Let's first see if you need to create a discussion 🧠🔮 _We will attempt to read your mind..._ + - **"Where are my academic benefits?"** Do you have this approved status image? You'll usually receive your benefits after 3 days of this approval status. Patience, my friend, patience! 😎 **"Awesome. But it's been 5 days?"** 😅 If you haven’t received your benefits within 5 days of your approval status, then yes, create this discussion and we'll assist! + - **Check out our [SDP FAQ](https://gh.io/sdp-faq-nd) and [Educators FAQ](https://github.com/orgs/community/discussions/145312)** – It has the most commonly asked questions when applying including: + * ⏱️ [How long after I’ve been approved will I receive my academic benefits?](https://gh.io/faq-nd-long) + * 📩 [I don't have a school-issued email address, what other official proof of enrollment is accepted?](https://github.com/orgs/community/discussions/111352#i-dont-have-a-school-issued-email-address) + * ✈️ 💻 [How to enable Copilot Pro for GitHub Education verified students/teachers?](https://gh.io/faq-nd-copilot) + ### Thanks, but I still want to join the conversation! 💬😄 _Love it! We're here for the banter_ + - **Can’t find your answer in our [SDP FAQ](https://gh.io/sdp-faq-nd)?** Then provide as much relevant information about your application as possible including posting your latest rejection reasons. + - **Calling all verified Educators** 📣 Did you know we have an exclusive teachers only discussions board you can access after being verified? 🤫 [Check it out here](https://github.com/community/Global-Campus-Teachers/discussions?new-dis) 👈 - #### Need Octernships project guidance? - * We **can not** provide specific project guidance and can only recommend you [reach out to your Octernships project maintainers](https://github.com/orgs/community/discussions/51456#discussioncomment-5542896) for instructions or to raise a bug. + Once you're ready to post, feel free to jump in. **We’re here to help!** 🌟 Ensure you have read our [Code of Conduct](https://github.com/community/community/blob/main/CODE_OF_CONDUCT.md). Remember **do not post** any personal information on the discussions boards e.g images of your evidence. - type: textarea id: conversation attributes: label: Hi everyone, - description: "Start your conversation below👇. Ensure you have read and adhere to our [Code of Conduct](https://github.com/community/community/blob/main/CODE_OF_CONDUCT.md). For more expectations around feedback responses - check out the [Community ReadMe](https://github.com/community/community#from-a-suggestion-to-a-shipped-feature)." + description: validations: required: true - type: markdown attributes: value: | - #### Anything else I should check before posting? 🤔 - * Check out the available documentation answering commonly asked questions: [Octernships FAQs](https://github.com/orgs/community/discussions/49380); [Student Developer Pack Application & FAQs](https://github.com/orgs/community/discussions/17814); [Campus Experts](https://education.github.com/students/experts). - * Search these boards for previous threads describing similar problems. ⭐ ***Hint:*** *search by rejection reason* - - type: markdown - attributes: - value: | - Remember to mark an [answer as "solved"](https://docs.github.com/en/discussions/collaborating-with-your-community-using-discussions/participating-in-a-discussion#marking-a-comment-as-an-answer) to help others with similar questions. ✅ + #### Anything else I could do to make my experience here more rewarding? 🤔 + * Remember to mark an [answer as "solved"](https://docs.github.com/en/discussions/collaborating-with-your-community-using-discussions/participating-in-a-discussion#marking-a-comment-as-an-answer) to help others with similar questions. ✅ + * Praise generously 💖 Thank members who have helped you and continue to support others within the community. ✨ diff --git a/.github/DISCUSSION_TEMPLATE/copilot.yml b/.github/DISCUSSION_TEMPLATE/npm.yml similarity index 95% rename from .github/DISCUSSION_TEMPLATE/copilot.yml rename to .github/DISCUSSION_TEMPLATE/npm.yml index ac3e7320d..fe53d944a 100644 --- a/.github/DISCUSSION_TEMPLATE/copilot.yml +++ b/.github/DISCUSSION_TEMPLATE/npm.yml @@ -1,4 +1,4 @@ -labels: [Copilot] +labels: [npm] body: - type: dropdown attributes: diff --git a/.github/DISCUSSION_TEMPLATE/projects-and-issues.yml b/.github/DISCUSSION_TEMPLATE/projects-and-issues.yml index 50c5080c9..b8953164b 100644 --- a/.github/DISCUSSION_TEMPLATE/projects-and-issues.yml +++ b/.github/DISCUSSION_TEMPLATE/projects-and-issues.yml @@ -6,7 +6,7 @@ body: feature areas [Issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/about-issues) and - [Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects) + [Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects). ⚠️ - type: dropdown attributes: @@ -40,17 +40,10 @@ body: label: Body description: Start your discussion! placeholder: >- - STOP! This category is only for discussions on GitHub Issues and - GitHub Projects. GitHub Issues are the issues you open in a repo to + STOP! GitHub Issues refers to the issues you open in a repo to track your work. NOT issues you are having with GitHub or a GitHub product. GitHub Projects are project boards you can create for example - Kanban boards, to track your work. NOT projects on GitHub. - - - We know the "issues" and ‘projects” naming conventions can be a little confusing. - - - To report an issue with another area of the GitHub, use the correct product category, if one does not exist, use the General category. To ensure you receive a timely response and connect with the proper team(s) make sure you are posting in the correct category. + Kanban boards, to track your work. NOT projects on GitHub. To ensure you receive a timely response by the correct team(s), make sure you are posting in the right category. validations: required: true - type: checkboxes diff --git a/.github/actions/add_feedback_comment b/.github/actions/add_feedback_comment index 183420f97..297be2369 100755 --- a/.github/actions/add_feedback_comment +++ b/.github/actions/add_feedback_comment @@ -39,5 +39,7 @@ puts "Checking #{node_id} for an existing feedback comment" exit unless Discussion.should_comment?(discussion_number: discussion_number, owner: owner, repo: repo) +puts "Adding a comment to the discussion" + discussion = Discussion.new(node_id) discussion.add_comment(body: body) diff --git a/.github/actions/close_incident_discussions.rb b/.github/actions/close_incident_discussions.rb new file mode 100755 index 000000000..7ed47a12b --- /dev/null +++ b/.github/actions/close_incident_discussions.rb @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../lib/github" +require_relative "../lib/discussions" +require "active_support" +require "active_support/core_ext/date_and_time/calculations" +require "active_support/core_ext/numeric/time" +require "date" + +# this action checks for any open incident discussions older than 2 days, and then closes them + +discussions = Discussion.find_open_incident_discussions(owner: "community", repo: "community").keep_if { |d| DateTime.parse(d.created_at) < 2.days.ago } + +if discussions.length == 0 + puts "No applicable discussions found, exiting" + exit +end + +discussions.each do |d| + # if a public summary has not been provided, find the most recent incident comment and mark it as the answer + unless d.is_answered + comment_id = d.find_most_recent_incident_comment_id(actor_login: "github-actions") + + unless comment_id.nil? + Discussion.mark_comment_as_answer(comment_id:) + end + + # an incident that has been declared as "resolved" should have already updated the post body, this is in case that step failed. + unless d.body.include?("https://github.com/community/community/blob/main/.github/src/incident_resolved.png?raw=true") + updated_body = "![A dark background with two security-themed abstract shapes positioned in the top left and bottom right corners. In the center of the image, bold white text reads \\\"Incident Resolved\\\" with a white Octocat logo.](https://github.com/community/community/blob/main/.github/src/incident_resolved.png?raw=true) \n #{d.body}" + d.update_discussion(body: updated_body) + end + end + + d.close_as_resolved +end diff --git a/.github/actions/open_incident_discussion.rb b/.github/actions/open_incident_discussion.rb new file mode 100755 index 000000000..ad67a15ec --- /dev/null +++ b/.github/actions/open_incident_discussion.rb @@ -0,0 +1,38 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../lib/github" +require_relative "../lib/discussions" +require "active_support/core_ext/date_time" + +# This script takes context from a received webhook and creates a new discussion in the correct discussion category + +repo_id = "MDEwOlJlcG9zaXRvcnkzMDE1NzMzNDQ=" +announcements_category_id = "DIC_kwDOEfmk4M4CQbR2" +incident_label_id = "LA_kwDOEfmk4M8AAAABpaZlTA" + +date = Time.now.strftime("%Y-%m-%d") + +# we need to take the provided input and generate a new post +title = "[#{date}] Incident Thread" + +body = <<~BODY +## :exclamation: An incident has been declared: + +**#{ENV['PUBLIC_TITLE']}** + +_Subscribe to this Discussion for updates on this incident. Please upvote or emoji react instead of commenting +1 on the Discussion to avoid overwhelming the thread. Any account guidance specific to this incident will be shared in thread and on the [Incident Status Page](#{ENV['INCIDENT_URL']})._ +BODY + +# we need to create a new discussion in the correct category with the correct label +begin + Discussion.create_incident_discussion( + repo_id:, + title:, + body:, + category_id: announcements_category_id, + labels: [incident_label_id] + ) +rescue => ArgumentError + puts "ERROR: One or more arguments missing. #{ArgumentError.message}" +end diff --git a/.github/actions/post_incident_summary.rb b/.github/actions/post_incident_summary.rb new file mode 100755 index 000000000..20b756fb0 --- /dev/null +++ b/.github/actions/post_incident_summary.rb @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../lib/github" +require_relative "../lib/discussions" + +# This script takes the public incident summary, adds it as a comment to the incident, and then marks that comment as the answer. + +# first, we must identify the correct incident to update, in the case where there are multiple open incident discussions. +selected_discussion = Discussion.find_open_incident_discussions(owner: "community", repo: "community").keep_if { |d| d.body.include?("#{ENV["INCIDENT_SLUG"]}") }.first + +if selected_discussion.nil? + puts "No applicable discussion, exiting" + exit +end + +# add the summary as a comment to the discussion +summary = "### Incident Summary \n #{ENV["INCIDENT_PUBLIC_SUMMARY"]}" +comment_id = selected_discussion.add_comment(body: summary).dig("data", "addDiscussionComment", "comment", "id") + +# mark this new comment as the answer +# (note that we don't need the discussion's context for this, so we don't call it on the instance of Discussion but on the struct) +Discussion.mark_comment_as_answer(comment_id:) + +# update the post body to include the resolved picture, but only if the post body has not already been updated. +unless selected_discussion.body.include?("https://github.com/community/community/blob/main/.github/src/incident_resolved.png?raw=true") + updated_body = "![A dark background with two security-themed abstract shapes positioned in the top left and bottom right corners. In the center of the image, bold white text reads \\\"Incident Resolved\\\" with a white Octocat logo.](https://github.com/community/community/blob/main/.github/src/incident_resolved.png?raw=true) \n \n #{selected_discussion.body}" + selected_discussion.update_discussion(body: updated_body) +end diff --git a/.github/actions/resolve_incident_discussion.rb b/.github/actions/resolve_incident_discussion.rb new file mode 100755 index 000000000..cafea375b --- /dev/null +++ b/.github/actions/resolve_incident_discussion.rb @@ -0,0 +1,20 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../lib/github" +require_relative "../lib/discussions" + +# This script acts when we receieve an incident resolved dispatch event to update the incident discussion. + +# first, we must identify the correct incident to update, in the case where there are multiple open incident discussions. +discussion = Discussion.find_open_incident_discussions(owner: "community", repo: "community").keep_if { |d| d.body.include?("#{ENV["INCIDENT_SLUG"]}") }.first + +if discussion.nil? + puts "No applicable discussion, exiting" + exit +end + +discussion.add_comment(body: "### Incident Resolved \n This incident has been resolved.") +# update the post body to include the resolved picture +updated_body = "![A dark background with two security-themed abstract shapes positioned in the top left and bottom right corners. In the center of the image, bold white text reads \\\"Incident Resolved\\\" with a white Octocat logo.](https://github.com/community/community/blob/main/.github/src/incident_resolved.png?raw=true) \n \n #{discussion.body}" +discussion.update_discussion(body: updated_body) diff --git a/.github/actions/update_incident_discussion.rb b/.github/actions/update_incident_discussion.rb new file mode 100755 index 000000000..8c14e7a75 --- /dev/null +++ b/.github/actions/update_incident_discussion.rb @@ -0,0 +1,20 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../lib/github" +require_relative "../lib/discussions" + +# This script takes the context from the latest update dispatch event and updates the active incident discussion + +# first, we must identify the correct incident to update, in the case where there are multiple open incident discussions. +discussion = Discussion.find_open_incident_discussions(owner: "community", repo: "community").keep_if { |d| d.body.include?("#{ENV["INCIDENT_SLUG"]}") }.first + +if discussion.nil? + puts "No applicable discussion, exiting" + exit +end + +# next, we need to update the discussion with the new information +update = "### Update \n #{ENV["INCIDENT_MESSAGE"]}" + +discussion.add_comment(body: update) diff --git a/.github/lib/discussions.rb b/.github/lib/discussions.rb index 9d9ae894f..5f9c607b2 100644 --- a/.github/lib/discussions.rb +++ b/.github/lib/discussions.rb @@ -8,7 +8,10 @@ :id, :url, :title, - :labelled + :labelled, + :body, + :created_at, + :is_answered ) do def self.all(owner: nil, repo: nil) return [] if owner.nil? || repo.nil? @@ -200,7 +203,7 @@ def add_comment(body: nil) mutation { addDiscussionComment( input: { - body: "#{body}", + body: """#{body}""", discussionId: "#{self.id}", clientMutationId: "rubyGraphQL" } @@ -309,10 +312,218 @@ def self.should_comment?(discussion_number: nil, owner: nil, repo: nil) } }.first + p response return false unless response[:labels].include?("Product Feedback") || response[:labels].include?("Bug") return false if response[:comments_by].include?("github-actions") true end + + def self.create_incident_discussion(repo_id:, title:, body:, category_id:, labels:) + # create a new discussion in the specified category, applies the incident label, and returns the discussion id + return if repo_id.nil? || title.nil? || body.nil? || category_id.nil? + + query = <<~QUERY + mutation { + createDiscussion( + input: { + categoryId: "#{category_id}", + repositoryId: "#{repo_id}", + clientMutationId: "rubyGraphQL", + title: "#{title}", + body: "#{body}" + } + ) { + clientMutationId + discussion { + id + body + } + } + } + QUERY + + incident_discussion_id = GitHub.new.mutate(graphql: query).dig("data", "createDiscussion", "discussion", "id") + + if labels + addLabel = <<~QUERY + mutation { + addLabelsToLabelable( + input: { + labelIds: #{labels}, + labelableId: "#{incident_discussion_id}", + clientMutationId: "rubyGraphQL" + } + ) { + clientMutationId + + } + } + QUERY + + GitHub.new.mutate(graphql: addLabel) + end + end + + def self.mark_comment_as_answer(comment_id:) + # marks the given comment as the answer + return if comment_id.nil? + + query = <<~QUERY + mutation { + markDiscussionCommentAsAnswer( + input: { + id: "#{comment_id}", + clientMutationId: "rubyGraphQL" + } + ) { + clientMutationId + discussion { + id + answer { + id + } + } + } + } + QUERY + + GitHub.new.mutate(graphql: query) + end + + def update_discussion(body:) + return if body.nil? + + query = <<~QUERY + mutation { + updateDiscussion( + input: { + discussionId: "#{self.id}", + body: "#{body}", + clientMutationId: "rubyGraphQL" + } + ) { + clientMutationId + discussion { + id + } + } + } + QUERY + + GitHub.new.mutate(graphql: query) + end + + def find_most_recent_incident_comment_id(actor_login:) + # finds the most recent comment generated by an incident action + return nil if actor_login.nil? + + query = <<~QUERY + query { + node(id: "#{self.id}") { + ... on Discussion { + id + comments(last: 10) { + nodes{ + id + createdAt + author { + login + } + } + } + } + } + } + QUERY + + # with the results, get an array of comments from the given actor login sorted by most recent + comments = GitHub.new.post(graphql: query).first.dig("comments", "nodes") + + return nil if comments.empty? + + filtered_comments = comments.keep_if { |comment| comment["author"] && comment["author"]["login"] == actor_login } + &.sort_by { |comment| comment["createdAt"] } + .reverse + + # return the most recent comment's ID + return nil if filtered_comments.empty? + filtered_comments.first["id"] + end + + def self.find_open_incident_discussions(owner:, repo:) + return [] if owner.nil? || repo.nil? + + searchquery = "repo:#{owner}/#{repo} is:open author:github-actions[bot] label:\\\"Incident \:exclamation\:\\\"" + + query = <<~QUERY + { + search( + first: 100 + query: "#{searchquery}" + type: DISCUSSION + ) { + discussionCount + ...Results + } + rateLimit { + limit + cost + remaining + resetAt + } + } + fragment Results on SearchResultItemConnection { + nodes { + ... on Discussion { + id + url + title + body + createdAt + isAnswered + } + } + } + QUERY + + GitHub.new.post(graphql: query) + .map! { |r| r.dig('nodes') } + .flatten + .map do |d| + Discussion.new( + d["id"], + d["url"], + d["title"], + false, # :labelled + d["body"], + d["createdAt"], + d["isAnswered"] + ) + end + end + + def close_as_resolved + # closes the post as resolved + + query = <<~QUERY + mutation { + closeDiscussion( + input: { + discussionId: "#{self.id}", + reason: RESOLVED, + clientMutationId: "rubyGraphQL" + } + ) { + clientMutationId + discussion { + id + } + } + } + QUERY + + GitHub.new.mutate(graphql: query) + end end diff --git a/.github/lib/github.rb b/.github/lib/github.rb index 6428abf75..6e8496faa 100644 --- a/.github/lib/github.rb +++ b/.github/lib/github.rb @@ -37,6 +37,7 @@ def post(graphql:) node = JSON.parse(response.body).dig("data", "repository") node = JSON.parse(response.body).dig("data", "search") if node.nil? + node = JSON.parse(response.body).dig("data", "node") if node.nil? # for when the query is not a repository or search nodes << node break unless node&.dig("pageInfo", "hasNextPage") @@ -51,6 +52,7 @@ def mutate(graphql:) response = @conn.post("/graphql") do |req| req.body = { query: graphql }.to_json end + p response JSON.parse(response.body) end diff --git a/.github/src/incident_resolved.png b/.github/src/incident_resolved.png new file mode 100644 index 000000000..efb1a1072 Binary files /dev/null and b/.github/src/incident_resolved.png differ diff --git a/.github/workflows/actions_labeller.yml b/.github/workflows/actions_labeller.yml new file mode 100644 index 000000000..929426d5c --- /dev/null +++ b/.github/workflows/actions_labeller.yml @@ -0,0 +1,311 @@ +name: Auto-Label Discussions for Actions Category + +on: + discussion: + types: [created] + +jobs: + label-actions-discussion: + if: ${{ contains(github.event.discussion.category.name, 'Actions') }} + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Get discussion body html + id: get_discussion_body_html + env: + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + run: | + gh api graphql -F owner=$OWNER -F name=$REPO -F number=$DISCUSSION_NUMBER -f query=' + query($owner: String!, $name: String!, $number: Int!) { + repository(owner: $owner, name: $name){ + discussion(number: $number) { + bodyHTML + id + } + } + }' > discussion_data.json + + echo 'DISCUSSION_BODY_HTML='$(jq -r '.data.repository.discussion.bodyHTML' discussion_data.json) >> $GITHUB_ENV + echo 'DISCUSSION_ID='$(jq -r '.data.repository.discussion.id' discussion_data.json) >> $GITHUB_ENV + + - run: npm install jsdom dompurify + + - name: Extract Title and Body Text + id: extract_text + uses: actions/github-script@v6 + env: + DISCUSSION_BODY_HTML: ${{ env.DISCUSSION_BODY_HTML }} + DISCUSSION_TITLE: ${{ github.event.discussion.title }} + with: + result-encoding: string + script: | + const jsdom = require('jsdom'); + const { JSDOM } = jsdom; + const { DISCUSSION_BODY_HTML } = process.env; + const fragment = JSDOM.fragment(DISCUSSION_BODY_HTML); + let body = ''; + const h3s = Array.from(fragment.querySelectorAll('h3')); + h3s.forEach(h3 => { + const heading = h3.textContent.trim(); + let p = h3.nextElementSibling; + while (p && p.tagName !== 'P') p = p.nextElementSibling; + if (!p) return; + if (heading === 'Discussion Details') { + body = p.textContent.trim(); + } + }); + body = body.replace(/^['\"]+|['\"]+$/g, ''); + const title = process.env.DISCUSSION_TITLE || ''; + core.info(`Extracted title: ${title}`); + core.info(`Extracted body: ${body}`); + return JSON.stringify({ title, body }); + + - name: Extract Primary and Secondary Topic Areas + id: extract_topics + uses: actions/github-script@v6 + env: + DISCUSSION_BODY_HTML: ${{ env.DISCUSSION_BODY_HTML }} + with: + result-encoding: string + script: | + const jsdom = require('jsdom'); + const { JSDOM } = jsdom; + const { DISCUSSION_BODY_HTML } = process.env; + const fragment = JSDOM.fragment(DISCUSSION_BODY_HTML); + let primary = ''; + let secondary = ''; + const h3s = Array.from(fragment.querySelectorAll('h3')); + h3s.forEach(h3 => { + const heading = h3.textContent.trim(); + let p = h3.nextElementSibling; + while (p && p.tagName !== 'P') p = p.nextElementSibling; + if (!p) return; + if (heading === 'Why are you starting this discussion?') { + primary = p.textContent.trim(); + } + if (heading === 'What GitHub Actions topic or product is this about?') { + secondary = p.textContent.trim(); + } + }); + core.info(`Extracted primary topic: ${primary}`); + core.info(`Extracted secondary topic: ${secondary}`); + return JSON.stringify({ primary, secondary }); + + - name: Auto-label by keyword search + id: auto_label_keywords + uses: actions/github-script@v6 + env: + EXTRACT_TEXT_RESULT: ${{ steps.extract_text.outputs.result }} + with: + result-encoding: string + script: | + const jsdom = require('jsdom'); + const { JSDOM } = jsdom; + const createDOMPurify = require('dompurify'); + const window = (new JSDOM('')).window; + const DOMPurify = createDOMPurify(window); + + const labelMap = [ + { + label: 'Workflow Deployment', + keywords: [ + "deployment error", + "publish artifact", + "release failure", + "deployment target", + "github pages", + "deployment issue", + "release workflow", + "target environment" + ] + }, + { + label: 'Workflow Configuration', + keywords: [ + "yaml syntax", + "job dependency", + "setup error", + "workflow file", + "configuration issue", + "matrix strategy", + "define env", + "secret management", + "environment setup", + "config job" + ] + }, + { + label: 'Schedule & Cron Jobs', + keywords: [ + "cron job", + "scheduled workflow", + "timing issue", + "delay trigger", + "timezone error", + "periodic run", + "recurring schedule", + "interval workflow", + "scheduled trigger", + "cron expression" + ] + }, + { + label: 'Metrics & Insights', + keywords: [ + "usage metrics", + "performance trend", + "analytics graph", + "stats dashboard", + "timeseries graph", + "insight report", + "metric tracking", + "workflow analytics", + "performance metric", + "statistics report" + ] + } + ]; + const miscLabel = 'Misc'; + let title = ''; + let body = ''; + try { + const parsed = JSON.parse(process.env.EXTRACT_TEXT_RESULT); + title = DOMPurify.sanitize(parsed.title || '', { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }).trim(); + body = DOMPurify.sanitize(parsed.body || '', { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }).trim(); + } catch (e) { + core.error('Failed to parse or sanitize discussion text: ' + e.message); + } + const text = (title + ' ' + body).toLowerCase(); + let foundLabel = miscLabel; + core.info(`Auto-label debug: text to match: '${text}'`); + for (const map of labelMap) { + core.info(`Auto-label debug: checking label '${map.label}' with keywords: ${map.keywords.join(', ')}`); + for (const k of map.keywords) { + if (text.includes(k)) { + core.info(`Auto-label debug: matched keyword '${k}' for label '${map.label}'`); + foundLabel = map.label; + break; + } + } + if (foundLabel !== miscLabel) break; + } + core.info(`Auto-label debug: selected label: '${foundLabel}'`); + return foundLabel; + + - name: Fetch label ID for primary topic + id: fetch_primary_label_id + env: + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + TOPIC: ${{ fromJson(steps.extract_topics.outputs.result).primary }} + run: | + echo "DEBUG: Fetching label for primary topic: $TOPIC" + gh api graphql -F owner=$OWNER -F name=$REPO -F topic="$TOPIC" -f query=' + query($owner: String!, $name: String!, $topic: String) { + repository(owner: $owner, name: $name) { + labels(first: 1, query: $topic) { + edges { + node { + id + name + } + } + } + } + } + ' > primary_label_data.json + + PRIMARY_LABEL_ID=$(jq -r '.data.repository.labels.edges[0]?.node?.id // empty' primary_label_data.json) + echo "PRIMARY_LABEL_ID=$PRIMARY_LABEL_ID" >> $GITHUB_ENV + + - name: Fetch label ID for secondary topic + id: fetch_secondary_label_id + env: + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + TOPIC: ${{ fromJson(steps.extract_topics.outputs.result).secondary }} + run: | + echo "DEBUG: Fetching label for secondary topic: $TOPIC" + gh api graphql -F owner=$OWNER -F name=$REPO -F topic="$TOPIC" -f query=' + query($owner: String!, $name: String!, $topic: String) { + repository(owner: $owner, name: $name) { + labels(first: 1, query: $topic) { + edges { + node { + id + name + } + } + } + } + } + ' > secondary_label_data.json + + SECONDARY_LABEL_ID=$(jq -r '.data.repository.labels.edges[0]?.node?.id // empty' secondary_label_data.json) + echo "SECONDARY_LABEL_ID=$SECONDARY_LABEL_ID" >> $GITHUB_ENV + + - name: Fetch label ID for auto-label + id: fetch_auto_label_id + env: + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + TOPIC: ${{ steps.auto_label_keywords.outputs.result }} + run: | + gh api graphql -F owner=$OWNER -F name=$REPO -F topic="$TOPIC" -f query=' + query($owner: String!, $name: String!, $topic: String) { + repository(owner: $owner, name: $name) { + labels(first: 1, query: $topic) { + edges { + node { + id + name + } + } + } + } + }' > auto_label_data.json + + AUTO_LABEL_ID=$(jq -r '.data.repository.labels.edges[0]?.node?.id // empty' auto_label_data.json) + echo "AUTO_LABEL_ID=$AUTO_LABEL_ID" >> $GITHUB_ENV + + - name: Apply labels to discussion + if: ${{ env.PRIMARY_LABEL_ID != '' || env.SECONDARY_LABEL_ID != '' || env.AUTO_LABEL_ID != '' }} + run: | + echo "DEBUG: PRIMARY_LABEL_ID=$PRIMARY_LABEL_ID" + echo "DEBUG: SECONDARY_LABEL_ID=$SECONDARY_LABEL_ID" + echo "DEBUG: AUTO_LABEL_ID=$AUTO_LABEL_ID" + LABEL_IDS=() + if [ -n "$PRIMARY_LABEL_ID" ]; then + LABEL_IDS+=("$PRIMARY_LABEL_ID") + fi + if [ -n "$SECONDARY_LABEL_ID" ]; then + LABEL_IDS+=("$SECONDARY_LABEL_ID") + fi + if [ -n "$AUTO_LABEL_ID" ]; then + LABEL_IDS+=("$AUTO_LABEL_ID") + fi + + # Deduplicate LABEL_IDS + LABEL_IDS=($(printf "%s\n" "${LABEL_IDS[@]}" | awk '!seen[$0]++')) + echo "DEBUG: LABEL_IDS to apply: ${LABEL_IDS[@]}" + + # Apply labels + gh api graphql -f query=' + mutation($labelableId: ID!, $labelIds: [ID!]!) { + addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) { + labelable { + labels(first: 10) { + edges { + node { + id + name + } + } + } + } + } + } + ' -f labelableId=$DISCUSSION_ID $(printf -- "-f labelIds[]=%s " "${LABEL_IDS[@]}") diff --git a/.github/workflows/close-dormant-discussions.yml b/.github/workflows/close-dormant-discussions.yml index 135b716c0..45bd9c1d0 100644 --- a/.github/workflows/close-dormant-discussions.yml +++ b/.github/workflows/close-dormant-discussions.yml @@ -3,8 +3,8 @@ name: Close out dormant discussions on: workflow_dispatch: schedule: - # At 01:23 every day - - cron: '23 1 * * *' + # At 01:25 every day + - cron: '25 1 * * *' jobs: build: diff --git a/.github/workflows/close-incident-discussions.yml b/.github/workflows/close-incident-discussions.yml new file mode 100644 index 000000000..1c18c538e --- /dev/null +++ b/.github/workflows/close-incident-discussions.yml @@ -0,0 +1,30 @@ +name: Close incident discussion after time elapsed + +on: + workflow_dispatch: + schedule: + # daily at midnight UTC + - cron: "0 0 * * *" + +jobs: + close_incident_post: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + discussions: write + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 + + - name: Bundle install + run: bundle install + + - name: Close incident discussions + id: close_discussions + run: .github/actions/close_incident_discussions.rb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/comment-on-dormant-discussions.yml b/.github/workflows/comment-on-dormant-discussions.yml index 929e08cf5..b4b812922 100644 --- a/.github/workflows/comment-on-dormant-discussions.yml +++ b/.github/workflows/comment-on-dormant-discussions.yml @@ -3,8 +3,8 @@ name: Comment on dormant discussions on: workflow_dispatch: schedule: - # At 03:23 every day - - cron: '23 3 * * *' + # At 03:25 every day + - cron: '25 3 * * *' jobs: build: diff --git a/.github/workflows/copilot_labeller.yml b/.github/workflows/copilot_labeller.yml new file mode 100644 index 000000000..e428a213a --- /dev/null +++ b/.github/workflows/copilot_labeller.yml @@ -0,0 +1,150 @@ +name: Copilot Templated Discussions + +on: + discussion: + types: [created] + +jobs: + label-copilot-discussion: + runs-on: ubuntu-latest + if: ${{ contains(github.event.discussion.category.name, 'Copilot') }} + + steps: + - name: Get discussion body html + id: get_discussion_body_html + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + run: | + gh api graphql -F owner=$OWNER -F name=$REPO -F number=$DISCUSSION_NUMBER -f query=' + query($owner: String!, $name: String!, $number: Int!) { + repository(owner: $owner, name: $name){ + discussion(number: $number) { + bodyHTML + id + } + } + }' > discussion_data.json + + echo 'DISCUSSION_BODY_HTML='$(jq -r '.data.repository.discussion.bodyHTML' discussion_data.json) >> $GITHUB_ENV + echo 'DISCUSSION_ID='$(jq -r '.data.repository.discussion.id' discussion_data.json) >> $GITHUB_ENV + - run: npm install jsdom + + - name: Get selected Copilot feature area + id: get_selected_feature_area + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + try { + const jsdom = require('jsdom'); + const { JSDOM } = jsdom; + const { DISCUSSION_BODY_HTML } = process.env + + const fragment = JSDOM.fragment(DISCUSSION_BODY_HTML); + const featureAreaHeaders = fragment.querySelectorAll("h3"); + const featureAreaHeader = Array.from(featureAreaHeaders).find(header => + header.textContent.trim().toLowerCase().includes('copilot feature area')); + if (!featureAreaHeader) { + return ""; + } + + const selectedAreaElement = featureAreaHeader.nextElementSibling; + if (!selectedAreaElement) { + return ""; + } + + const selectedArea = selectedAreaElement.textContent.trim(); + // Simplify area matching by converting to lowercase + const selectedAreaLower = selectedArea.toLowerCase(); + + // Valid Copilot feature areas (case insensitive matching) + const validAreas = { + "vs code": "VS Code", + "visual studio": "Visual Studio", + "jetbrains & xcode": "JetBrains & Xcode", + "copilot in github": "Copilot in GitHub", + "copilot workspace": "Copilot Workspace", + "copilot edits and code review": "Copilot Edits and Code Review", + "copilot agent mode": "Copilot Agent Mode", + "copilot coding agent": "Copilot Coding Agent", + "copilot enterprise": "Copilot Enterprise", + "copilot billing or account‑related": "Copilot Billing or Account‑Related", + "other copilot areas": "Other Copilot Areas" + }; + + // Try to find a matching area (case insensitive) + for (const [key, value] of Object.entries(validAreas)) { + if (selectedAreaLower.includes(key)) { + return value; // Return the properly cased label + } + } + + // If no match found, check if it's exactly one of our valid areas + // This helps with unexpected formatting or spacing + if (selectedArea && Object.values(validAreas).includes(selectedArea)) { + return selectedArea; + } + + // Default to "Other Copilot Areas" if we can't find a specific match + if (selectedArea) { + return "Other Copilot Areas"; + } + + return ""; + } catch (error) { + console.error(error); + return ""; + } + + - name: Fetch label id for selected area + id: fetch_label_id + if: ${{ steps.get_selected_feature_area.outputs.result != '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + AREA: ${{ steps.get_selected_feature_area.outputs.result }} + run: | + gh api graphql -F owner=$OWNER -F name=$REPO -F topic="$AREA" -f query=' + query($owner: String!, $name: String!, $topic: String) { + repository(owner: $owner, name: $name){ + labels(first: 1, query: $topic) { + edges { + node { + id + name + } + } + } + } + }' > repository_label_data.json + + LABEL_ID=$(jq -r '.data.repository.labels.edges[0]?.node?.id // empty' repository_label_data.json) + if [ -z "$LABEL_ID" ]; then + echo "No matching label found for the selected area. Skipping labeling step." + fi + echo "LABEL_ID=$LABEL_ID" >> $GITHUB_ENV + + - name: Label the discussion + if: ${{ steps.get_selected_feature_area.outputs.result != '' && env.LABEL_ID != '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api graphql -f query=' + mutation($labelableId: ID!, $labelIds: [ID!]!) { + addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) { + labelable { + labels(first: 10) { + edges { + node { + id + name + } + } + } + } + } + }' -f labelableId=$DISCUSSION_ID -f labelIds[]=$LABEL_ID diff --git a/.github/workflows/open-incident-discussion.yml b/.github/workflows/open-incident-discussion.yml new file mode 100644 index 000000000..3b0067941 --- /dev/null +++ b/.github/workflows/open-incident-discussion.yml @@ -0,0 +1,30 @@ +name: Open a new discussion when an incident is declared + +on: + repository_dispatch: + types: [incident-declared] + +jobs: + open_discussion: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + discussions: write + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 + + - name: Bundle install + run: bundle install + + - name: Open discussion + id: open_discussion + run: .github/actions/open_incident_discussion.rb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PUBLIC_TITLE: ${{ github.event.client_payload.publicTitle }} + INCIDENT_URL: ${{ github.event.client_payload.incidentLink }} diff --git a/.github/workflows/post-incident-summary.yml b/.github/workflows/post-incident-summary.yml new file mode 100644 index 000000000..a86b016c7 --- /dev/null +++ b/.github/workflows/post-incident-summary.yml @@ -0,0 +1,40 @@ +name: Post incident summary to incident discussion + +on: + repository_dispatch: + types: [incident-public-summary] + +jobs: + post_summary: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + discussions: write + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 + + - name: Bundle install + run: bundle install + + - name: Get incident URL slug + id: incident_slug + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const url = "${{ github.event.client_payload.incidentLink }}" + const slug = url.split('/').pop(); + return slug + + - name: Add comment to incident discussion + id: add_comment + run: .github/actions/post_incident_summary.rb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INCIDENT_SLUG: ${{ steps.incident_slug.outputs.result }} + INCIDENT_PUBLIC_SUMMARY: ${{ github.event.client_payload.message }} diff --git a/.github/workflows/update-incident-discussion-as-resolved.yml b/.github/workflows/update-incident-discussion-as-resolved.yml new file mode 100644 index 000000000..f092a467f --- /dev/null +++ b/.github/workflows/update-incident-discussion-as-resolved.yml @@ -0,0 +1,39 @@ +name: Mark incident discussion as resolved + +on: + repository_dispatch: + types: [incident-resolved] + +jobs: + resolve_discussion: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + discussions: write + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 + + - name: Bundle install + run: bundle install + + - name: Get incident URL slug + id: incident_slug + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const url = "${{ github.event.client_payload.incidentLink }}" + const slug = url.split('/').pop(); + return slug + + - name: Mark incident discussion as resolved + id: mark_resolved + run: .github/actions/resolve_incident_discussion.rb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INCIDENT_SLUG: ${{ steps.incident_slug.outputs.result }} \ No newline at end of file diff --git a/.github/workflows/update-incident-discussion.yml b/.github/workflows/update-incident-discussion.yml new file mode 100644 index 000000000..63ce19fdc --- /dev/null +++ b/.github/workflows/update-incident-discussion.yml @@ -0,0 +1,40 @@ +name: Update current incident discussion with new information + +on: + repository_dispatch: + types: [incident-public-update] + +jobs: + update_discussion: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + discussions: write + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 + + - name: Bundle install + run: bundle install + + - name: Get incident URL slug + id: incident_slug + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const url = "${{ github.event.client_payload.incidentLink }}" + const slug = url.split('/').pop(); + return slug + + - name: Add comment to incident discussion + id: add_comment + run: .github/actions/update_incident_discussion.rb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INCIDENT_SLUG: ${{ steps.incident_slug.outputs.result }} + INCIDENT_MESSAGE: ${{ github.event.client_payload.message }} diff --git a/.ruby-version b/.ruby-version index ef538c281..fa7adc7ac 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.2 +3.3.5 diff --git a/Gemfile.lock b/Gemfile.lock index 7f38f5b9f..7e6da7695 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,17 +17,20 @@ GEM concurrent-ruby (1.2.3) connection_pool (2.4.1) drb (2.2.1) - faraday (2.9.0) - faraday-net_http (>= 2.0, < 3.2) - faraday-net_http (3.1.0) - net-http + faraday (2.12.1) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.0) + net-http (>= 0.5.0) i18n (1.14.4) concurrent-ruby (~> 1.0) json (2.7.1) language_server-protocol (3.17.0.3) + logger (1.6.1) minitest (5.22.2) mutex_m (0.2.0) - net-http (0.4.1) + net-http (0.5.0) uri parallel (1.24.0) parser (3.3.0.5) @@ -37,7 +40,7 @@ GEM rack (3.0.9.1) rainbow (3.1.1) regexp_parser (2.9.0) - rexml (3.3.1) + rexml (3.3.6) strscan rubocop (1.62.0) json (~> 2.3) @@ -69,10 +72,12 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) - uri (0.13.0) + uri (1.0.2) PLATFORMS arm64-darwin-21 + arm64-darwin-24 + x86_64-linux DEPENDENCIES faraday diff --git a/README.md b/README.md index fbbb5d566..0116bfb82 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ In this repository, you will find categories for various product areas. Feel fre |--- |--- | | 👍 [Accessibility](https://github.com/orgs/community/discussions/categories/accessibility) | [About Accessibility](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-personal-account-settings/managing-accessibility-settings#about-accessibility-settings) | | 🚢 [Actions](https://github.com/orgs/community/discussions/categories/actions) | [GitHub Actions](https://github.com/features/actions) | -| 🔁 [API and Webhooks](https://github.com/orgs/community/discussions/categories/api-and-webhooks) | [GitHub API](https://docs.github.com/en/rest) and [GitHub Webhooks](https://docs.github.com/en/developers/webhooks-and-events/webhooks/about-webhook) | +| 🔁 [API and Webhooks](https://github.com/orgs/community/discussions/categories/api-and-webhooks) | [GitHub REST API](https://docs.github.com/en/rest), [GitHub GraphQL API](https://docs.github.com/en/graphql), and [GitHub Webhooks](https://docs.github.com/en/developers/webhooks-and-events/webhooks/about-webhook) | | 🔎 [Code Search & Navigation](https://github.com/orgs/community/discussions/categories/code-search-and-navigation) | [Code Search & Navigation](https://cs.github.com/about) | | 💻 [Codespaces](https://github.com/orgs/community/discussions/categories/codespaces) | [GitHub Codespaces](https://github.com/features/codespaces) | -| 👩‍✈️ [Copilot](https://github.com/orgs/community/discussions/categories/copilot) | [GitHub Copilot](https://copilot.github.com/) | +| :copilot: [Copilot Conversations](https://github.com/orgs/community/discussions/categories/copilot-conversations) | [GitHub Copilot](https://copilot.github.com/) | | 🤖 [Code Security](https://github.com/orgs/community/discussions/categories/code-security) | [GitHub Code Security](https://github.com/features/security) | | 🗣️ [Discussions](https://github.com/orgs/community/discussions/categories/discussions) | [GitHub Discussions](https://docs.github.com/en/discussions) | | 🌐 [Feed](https://github.com/orgs/community/discussions/categories/feed) | [GitHub Feed](https://github.blog/2022-03-22-improving-your-github-feed/) | diff --git a/docs/Accessibility Skills Student Series Spring 2025.md b/docs/Accessibility Skills Student Series Spring 2025.md new file mode 100644 index 000000000..34161747a --- /dev/null +++ b/docs/Accessibility Skills Student Series Spring 2025.md @@ -0,0 +1,73 @@ +### Accessibility Skills Student Series Spring 2025 Random Prize Draw Terms and Conditions + +**1. Promoter** +The promoter of this prize draw is GitHub Education, a division of GitHub, Inc., located at 88 Colin P. Kelly Jr. Street, San Francisco, CA 94107, USA. + +**2. Eligibility** + +The prize draw is open to verified registered students of GitHub Education who are at least 13 years old. +- Participants under 18 years must have parental or guardian consent to enter. +- Employees of GitHub, Microsoft, their immediate families, and anyone otherwise connected with the organization are not eligible to enter. +- The prize draw is void in countries where such promotions are prohibited by law. This includes, but is not limited to, the following countries: + - Belgium + - Poland + - Malaysia + - Brunei + - Bangladesh + - Italy + - Any other country where local laws prohibit free entry random prize draws + +**3. Entry Period** + +- The entry period begins on April 14, 2025, 11:59 PM EST and ends on May 15, 2025, at 11:59 PM EST. + +**4. How to Enter** + +- To to receive an entry, participants must visit the designated forum discussion and complete the challenge for that week + +**5. Free Entry** + +- Participation in the prize draw is free, and no purchase is necessary to enter or win. + +**6. Parental/Guardian Consent** + +- If you are under 18 years old, you must have parental or guardian consent to enter the prize draw. By entering, you confirm that you have obtained such consent. + +**7. Selection of Winners** + +- Up to 10 winners will be selected at random from all eligible entries between May 15 and May 22, 2025. +- The draw will be conducted using a verifiably random process under the supervision of an independent person or by an independent entity. + +**8. Notification of Winners** + +- Winners will be notified via the email associated with their GitHub account by May 31, 2025. +- Winners must respond to the notification within 7 days to claim their prize. Failure to respond within this period may result in forfeiture of the prize, and GitHub Education reserves the right to select an alternative winner. + +**9. Prizes** + +- Each winner will receive GitHub swag, which may include items such as T-shirts, stickers, or other branded merchandise. +- Prizes are non-transferable and no cash alternative is available. +- GitHub Education reserves the right to substitute any prize with another of equivalent value without giving notice. + +**10. Data Protection** + +- Participants’ personal data will be used solely for the purposes of administering the prize draw and in accordance with GitHub’s privacy policy. + +**11. General Conditions** + +- GitHub Education reserves the right to cancel or amend the prize draw and these terms and conditions without notice in the event of a catastrophe, war, civil or military disturbance, or any actual or anticipated breach of any applicable law or regulation or any other event outside GitHub Education’s control. +- GitHub Education is not responsible for inaccurate prize details supplied to any participant by any third party connected with this prize draw. + +**12. Governing Law** + +- This prize draw and these terms and conditions will be governed by the laws of the country where the participant resides, provided that such laws do not conflict with the laws of the United States. Any disputes will be subject to the non-exclusive jurisdiction of the courts of the country of the participant’s residence. + +**13. Limitation of Liability** + +- By entering, participants agree to release and hold harmless GitHub Education, GitHub, Inc., and Microsoft from any liability, illness, injury, death, loss, litigation, claim, or damage that may occur, directly or indirectly, whether caused by negligence or not, from such entrant’s participation in the prize draw and/or their acceptance, possession, use, or misuse of any prize or any portion thereof. + +**14. Sponsor** + +- The prize draw is sponsored by GitHub Education. + +For further information or any questions regarding the prize draw, please contact [morganersery1@github.com]. diff --git a/docs/Meet the Maintainers Sweepstakes Rules.md b/docs/Meet the Maintainers Sweepstakes Rules.md new file mode 100644 index 000000000..b4dca0250 --- /dev/null +++ b/docs/Meet the Maintainers Sweepstakes Rules.md @@ -0,0 +1,74 @@ +### Meet the Maintainers March 2025 Random Prize Draw Terms and Conditions + +**1. Promoter** +The promoter of this prize draw is GitHub Community, a division of GitHub, Inc., located at 88 Colin P. Kelly Jr. Street, San Francisco, CA 94107, USA. + +**2. Eligibility** + +The prize draw is open to verified registered GitHub users who are at least 13 years old. +- Employees of GitHub, Microsoft, their immediate families, and anyone otherwise connected with the organization are not eligible to enter. +- Participants under 18 years must have parental or guardian consent to enter. +- The prize draw is void in countries where such promotions are prohibited by law. This includes, but is not limited to, the following countries: + - Belgium + - Poland + - Malaysia + - Brunei + - Bangladesh + - Italy + - Any other country where local laws prohibit free entry random prize draws + +**3. Entry Period** + +- The entry period begins on March 11, 2025 11:59 PM EST and ends on March 18, 2025, at 11:59 PM EST. + +**4. How to Enter** + +- To enter, participants must visit the designated forum discussion and ask a question. +- Entries must be original and not generated entirely by AI tools. AI tools may be used for grammar and spelling checks only. + +**5. Free Entry** + +- Participation in the prize draw is free, and no purchase is necessary to enter or win. + +**6. Parental/Guardian Consent** + +- If you are under 18 years old, you must have parental or guardian consent to enter the prize draw. By entering, you confirm that you have obtained such consent. + +**7. Selection of Winners** + +- Up to 5 winners will be selected at random from all eligible entries between March 11 and March 18, 2025. +- The draw will be conducted using a verifiably random process under the supervision of an independent person or by an independent entity. + +**8. Notification of Winners** + +- Winners will be notified via the discussion by March 25, 2025. +- Winners must fill out the form within 7 days to claim their prize. Failure to respond within this period may result in forfeiture of the prize, and GitHub Education reserves the right to select an alternative winner. + +**9. Prizes** + +- Each winner will receive a Github Shop voucher +- Prizes are non-transferable and no cash alternative is available. +- GitHub Community reserves the right to substitute any prize with another of equivalent value without giving notice. + +**10. Data Protection** + +- Participants’ personal data will be used solely for the purposes of administering the prize draw and in accordance with GitHub’s privacy policy. + +**11. General Conditions** + +- GitHub Community reserves the right to cancel or amend the prize draw and these terms and conditions without notice in the event of a catastrophe, war, civil or military disturbance, or any actual or anticipated breach of any applicable law or regulation or any other event outside GitHub Education’s control. +- GitHub Community is not responsible for inaccurate prize details supplied to any participant by any third party connected with this prize draw. + +**12. Governing Law** + +- This prize draw and these terms and conditions will be governed by the laws of the country where the participant resides, provided that such laws do not conflict with the laws of the United States. Any disputes will be subject to the non-exclusive jurisdiction of the courts of the country of the participant’s residence. + +**13. Limitation of Liability** + +- By entering, participants agree to release and hold harmless GitHub Community, GitHub, Inc., and Microsoft from any liability, illness, injury, death, loss, litigation, claim, or damage that may occur, directly or indirectly, whether caused by negligence or not, from such entrant’s participation in the prize draw and/or their acceptance, possession, use, or misuse of any prize or any portion thereof. + +**14. Sponsor** + +- The prize draw is sponsored by GitHub Community. + +For further information or any questions regarding the prize draw, please contact [queenofcorgis@github.com]. diff --git a/docs/Responsible-AI-for-Open-Source-Sweepstakes-Official-Rules.md b/docs/Responsible-AI-for-Open-Source-Sweepstakes-Official-Rules.md new file mode 100644 index 000000000..535e2d1ce --- /dev/null +++ b/docs/Responsible-AI-for-Open-Source-Sweepstakes-Official-Rules.md @@ -0,0 +1,69 @@ +# MICROSOFT RESPONSIBLE AI FOR OPEN SOURCE SWEEPSTAKES +## OFFICIAL RULES + +### SPONSOR +These Official Rules (“Rules”) govern the operation of the Microsoft Responsible AI for Open Source Sweepstakes (“Sweepstakes”). Microsoft Corporation, One Microsoft Way, Redmond, WA, 98052, USA, is the Sweepstakes sponsor (“Sponsor”). + +### DEFINITIONS +In these Rules, "Microsoft", "we", "our", and "us" refer to Sponsor and “you” and "yourself" refers to a Sweepstakes participant, or the parent/legal guardian of any Sweepstakes entrant who has not reached the age of majority to contractually obligate themselves in their legal place of residence. By entering you (your parent/legal guardian if you are not the age of majority in your legal place of residence) agree to be bound by these Rules. + +### ENTRY PERIOD +The Sweepstakes starts at 12:00 a.m. Pacific Time (PT) on July 24, 2025, and ends at 11:59 p.m. PT on August 21, 2025 (“Entry Period”). + +### ELIGIBILITY +To enter, you must be 18 years of age or older. If you are 18 years of age or older but have not reached the age of majority in your legal place of residence, then you must have consent of a parent/legal guardian. + +Employees and directors of Microsoft Corporation and its subsidiaries, affiliates, advertising agencies, and Sweepstakes Parties are not eligible, nor are persons involved in the execution or administration of this promotion, or the family members of each above (parents, children, siblings, spouse/domestic partners, or individuals residing in the same household). Void in Cuba, Iran, North Korea, Sudan, Syria, Region of Crimea, Russia, and where prohibited. + +### HOW TO ENTER +No Purchase Necessary. + +You will receive one Sweepstakes entry when you participate in the marked discussion on the GitHub Community during our entry period. There will be three (3) different challenges in total where you’ll be able to comment on the discussion stating that you completed the challenge. Challenges vary from sharing insights of codebase deep dive with AI, rewriting a Pull Request, and posting your learnings from an AI-generated code review. To participate in the GitHub Community, you must have a free GitHub account which you can sign up for at GitHub.com. + +The entry limit is one entry per challenge (there are three challenges). + +Any attempt by you to obtain more than the stated number of entries by using multiple/different accounts, identities, registrations, logins, or any other methods will void your entries and you may be disqualified. Use of any automated system to participate is prohibited. + +We are not responsible for excess, lost, late, or incomplete entries. If disputed, entries will be deemed submitted by the “authorized account holder” of the email address, social media account, or other method used to enter. The “authorized account holder” is the natural person assigned to an email address by an internet or online service provider, or other organization responsible for assigning email addresses. + +### WINNER SELECTION AND NOTIFICATION +Pending confirmation of eligibility, potential prize winners will be selected by Microsoft or their Agent in a random drawing from among all eligible entries received within 7 days following the Entry Period. + +Winners will be notified via the contact information provided during entry no more than 7 days following the drawing with prize claim instructions, including submission deadlines. If a selected winner cannot be contacted, is ineligible, fails to claim a prize or fails to return any forms, the selected winner will forfeit their prize and an alternate winner will be selected time allowing. If you are a potential winner and you are 18 or older but have not reached the age of majority in your legal place of residence, we may require your parent/legal guardian to sign all required forms on your behalf. Only three alternate winners will be selected, after which unclaimed prizes will remain unawarded. + +### PRIZES +The following prizes will be awarded: + +Ten (10) Grand Prizes. Each winner will receive: +A(n) GitHub Shop voucher. Approximate Retail Value (ARV) $30.00. + +The ARV of electronic prizes is subject to price fluctuations in the consumer marketplace based on, among other things, any gap in time between the date the ARV is estimated for purposes of these Official Rules and the date the prize is awarded or redeemed. We will determine the value of the prize to be the fair market value at the time of prize award. + +The total Approximate Retail Value (ARV) of all prizes: $300 + +We will only award one (1) prize per person/company during the Entry Period. No more than the stated number of prizes will be awarded. No substitution, transfer, or assignment of prize permitted, except that Microsoft reserves the right to substitute a prize of equal or greater value in the event the offered prize is unavailable. Microsoft products awarded as prizes are awarded “AS IS” and WITHOUT WARRANTY OF ANY KIND, express or implied (including any implied warranty of merchantability or fitness for a particular purpose); you assume the entire risk of quality and performance, and should the prizes prove defective, you assume the entire cost of all necessary servicing or repair. This is so even if the Microsoft product mentions a warranty on its packaging, in a manual, or in marketing materials; no warranty applies to Microsoft products awarded as prizes. + +Microsoft does not give any warranty of any kind, express or implied (including any implied warranty of merchantability or fitness for a particular purpose) on products made by a company other than Microsoft that are awarded as prizes. Please contact the manufacturer to see if it is covered by that company’s warranty. + +Prizes will be sent no later than 28 days after winner selection. Prize winners may be required to complete and return prize claim and/or tax forms (“Forms”) within the deadline stated in the winner notification. Taxes on the prize, if any, are the sole responsibility of the winner, who is advised to seek independent counsel regarding the tax implications of accepting a prize. By accepting a prize, you agree that Microsoft may use your entry, name, image and hometown online and in print, or in any other media, in connection with this Sweepstakes without payment or compensation to you, except where prohibited by law. + +### ODDS +The odds of winning are based on the number of eligible entries received. + +### GENERAL CONDITIONS AND RELEASE OF LIABILITY +To the extent allowed by law, by entering you agree to release and hold harmless Microsoft and its respective parents, partners, subsidiaries, affiliates, employees, and agents from any and all liability or any injury, loss, or damage of any kind arising in connection with this Sweepstakes or any prize won. + +All local laws apply. The decisions of Microsoft are final and binding. + +We reserve the right to cancel, change, or suspend this Sweepstakes for any reason, including cheating, technology failure, catastrophe, war, or any other unforeseen or unexpected event that affects the integrity of this Sweepstakes, whether human or mechanical. If the integrity of the Sweepstakes cannot be restored, we may select winners from among all eligible entries received before we had to cancel, change or suspend the Sweepstakes. + +If you attempt or we have strong reason to believe that you have compromised the integrity or the legitimate operation of this Sweepstakes by cheating, hacking, creating a bot or other automated program, or by committing fraud in any way, we may seek damages from you to the full extent of the law and you may be banned from participation in future Microsoft promotions. + +### USE OF YOUR ENTRY +Personal data you provide while entering this Sweepstakes will be used by Microsoft and/or its agents and prize fulfillers acting on Microsoft’s behalf only for the administration and operation of this Sweepstakes and in accordance with the Microsoft Privacy Statement. + +### GOVERNING LAW +This Sweepstakes will be governed by the laws of the State of Washington, and you consent to the exclusive jurisdiction and venue of the courts of the State of Washington for any disputes arising out of this Sweepstakes. + +### WINNERS LIST +Send an email to lgalante@microsoft.com with the subject line “Responsible AI for Open Source Sweepstakes winners” within 30 days of August 21, 2025 to receive a list of winners that received a prize worth $25.00 or more. diff --git a/docs/Spring 2025 GitHub Foundation Certification Prep Course Sweepstakes Official Rules.md b/docs/Spring 2025 GitHub Foundation Certification Prep Course Sweepstakes Official Rules.md new file mode 100644 index 000000000..b399a7b24 --- /dev/null +++ b/docs/Spring 2025 GitHub Foundation Certification Prep Course Sweepstakes Official Rules.md @@ -0,0 +1,115 @@ +# MICROSOFT SPRING 2025 GITHUB FOUNDATIONS CERTIFICATION PREP COURSE SWEEPSTAKES + +## OFFICIAL RULES + +--- + +## SPONSOR + +These Official Rules ("Rules") govern the operation of the Microsoft Spring 2025 GitHub Foundations Certification Prep Course Sweepstakes ("Sweepstakes"). Microsoft Corporation, One Microsoft Way, Redmond, WA, 98052, USA, is the Sweepstakes sponsor ("Sponsor"). + +--- + +## DEFINITIONS + +In these Rules, "Microsoft", "we", "our", and "us" refer to Sponsor and "you" and "yourself" refers to a Sweepstakes participant, or the parent/legal guardian of any Sweepstakes entrant who has not reached the age of majority to contractually obligate themselves in their legal place of residence. By entering you (your parent/legal guardian if you are not the age of majority in your legal place of residence) agree to be bound by these Rules. + +--- + +## ENTRY PERIOD + +The Sweepstakes starts at **12:00 a.m. Pacific Time (PT) on March 31, 2025**, and ends at **11:59 p.m. PT on May 2, 2025** and will consist of five (5) weekly Entry Periods as follows (each an "Entry Period"): + +- **March 31, 2025, 12:00 a.m. PT – April 4, 2025, 11:59 p.m. PT** +- **April 5, 2025, 12:00 a.m. PT – April 11, 2025, 11:59 p.m. PT** +- **April 12, 2025, 12:00 a.m. PT – April 18, 2025, 11:59 p.m. PT** +- **April 19, 2025, 12:00 a.m. PT – April 25, 2025, 11:59 p.m. PT** +- **April 26, 2025, 12:00 a.m. PT – May 2, 2025, 11:59 p.m. PT** + +--- + +## ELIGIBILITY + +To enter, you must be **14 years of age or older** with an **active GitHub account**. If you are 14 years of age or older but have not reached the age of majority in your legal place of residence, then you must have consent of a parent/legal guardian. + +**Not eligible:** Employees and directors of Microsoft Corporation, its subsidiaries, affiliates, advertising agencies, and Sweepstakes Parties. Void in Cuba, Iran, North Korea, Sudan, Syria, Region of Crimea, Russia, and where prohibited. + +--- + +## HOW TO ENTER + +**No Purchase Necessary.** + +You will receive **one entry** by visiting the web site for the Sweepstakes at [GitHub Community Discussions](https://github.com/orgs/community/discussions/) and completing any of the following tasks: + +- Answer a knowledge check +- Share resources +- Ask questions +- Create a study guide + +**Entry limit:** One per person per weekly Entry Period. + +Attempts to submit multiple entries using multiple accounts or automated methods will result in disqualification. + +--- + +## WINNER SELECTION AND NOTIFICATION + +Pending confirmation of eligibility, **twenty-five (25) potential prize winners** will be selected by Microsoft or their Agent in a random drawing from among all eligible entries within **7 days** following the end of the Entry Period. + +Winners will be notified within **7 days** following the drawing. If a selected winner cannot be contacted, is ineligible, or fails to claim their prize, an alternate winner will be selected. A maximum of three alternate winners will be chosen before prizes go unawarded. + +--- + +## PRIZES + +### Twenty-Five (25) Grand Prizes + +Each winner will receive a **GitHub Certification voucher**. + +- **Approximate Retail Value (ARV):** $50.00 +- **Total ARV of all prizes:** $1,250 + +Only one (1) prize per person overall. **No substitution, transfer, or assignment of prize permitted.** Microsoft reserves the right to substitute a prize of equal or greater value if the offered prize is unavailable. + +Prizes will be sent no later than **28 days after winner selection**. + +**Taxes on the prize are the sole responsibility of the winner.** By accepting a prize, you agree that Microsoft may use your entry, name, image, and hometown online and in print in connection with this Sweepstakes. + +--- + +## ODDS + +The odds of winning depend on the number of eligible entries received. + +--- + +## GENERAL CONDITIONS AND RELEASE OF LIABILITY + +To the extent allowed by law, by entering you agree to release and hold harmless **Microsoft**, its parents, partners, subsidiaries, affiliates, employees, and agents from any liability related to this Sweepstakes. + +Microsoft reserves the right to **cancel, change, or suspend** the Sweepstakes due to unforeseen circumstances affecting the integrity of the contest. + +If you attempt to **cheat, hack, or commit fraud**, Microsoft may disqualify you and seek damages. + +--- + +## USE OF YOUR ENTRY + +Personal data provided while entering this Sweepstakes will be used **only for administration and operation** of this Sweepstakes in accordance with the [Microsoft Privacy Statement](https://privacy.microsoft.com/). + +--- + +## GOVERNING LAW + +This Sweepstakes is governed by the **laws of the State of Washington**, and participants consent to the **exclusive jurisdiction and venue** of its courts for any disputes. + +--- + +## WINNERS LIST + +To receive a list of winners, send an email to **lgalante@microsoft.com** with the subject line: + +> *Spring 2025 GitHub Foundations Certification Prep Course Sweepstakes winners* + +within **30 days of May 2, 2025**.